This massive and comprehensive Python API course is taught by Sanjeev
Thiagarajan.
He's an excellent teacher and has taught many popular courses. Hey, how's it going, everyone?
I just wanted to welcome you to my Python API development course.
So in this course, I'm going to walk you through building out your very own API in Python.
However, keep in mind, this course is so much more than just building out a simple API. In
fact,
as I'm sure you saw the video length, this course does come out to a whopping 19 hours in
length. And so you're probably wondering, well, what exactly are we going to be covering for 19
hours?
We don't need 19 hours to create an API. Well, first of all, we're going to build out a fully
featured API that includes authentication, current operations, scheme of validation,
and we're going to set up documentation for our API, because that's very important. However,
the learning doesn't stop just there. This course is going to extend well past just
basic API development. We'll also learn all of the tooling that surrounds building a complete
and robust API. I've dedicated a large section of this course to learning SQL. And I've
noticed
that a lot of the other API and web development courses, they just quickly gloss over SQL
without diving into the nitty gritty of how SQL databases work. For this course, we're going to cover
SQL
extensively. And we're going to start from the absolute basics. So you don't need to know a
single thing about databases or SQL in general. But by the time you complete this course,
you
will be very proficient at generating database schemas, you'll know core SQL concepts like
primary keys, foreign keys, table constraints, and you can pretty much be able to generate
SQL
queries to grab the exact data that you're looking for. And on top of that, I'll show you how
to integrate your SQL databases in your API using two different methods. So we'll cover both using
raw
SQL queries as well as ORMs so that no matter which method you ultimately prefer, you'll
have
all the skills and resources to start building out your own projects. We'll also
familiarize
ourselves with database migration tools like alembic, which allow us to make incremental
changes to our database schema to track changes and get just like we can with our regular
Python
code. We'll also learn how to use tools like postman to construct HTTP packets so that
we
could test our API during the whole development process. And when it comes to testing, I've
added about you know, two to three hours of content going over how to set up automated
integration
tests so that when you make changes to your code, you can run these automated tests to verify
that your code changes haven't broke any pre existing functionality. And then after testing, we're
going
to move on to the deployment phase, we're going to actually deploy our application. And I
didn't just include one method of deployment, I've actually included two different deployment
scenarios.
The first scenario is probably the most common scenario, which is deploying our app onto like
an Ubuntu machine that can be hosted on any cloud provider like AWS, GCP, Azure, or even
digital
ocean. And you know, we'll cover things like how to set up nginx to act as a reverse proxy,
we'll, we'll configure our own system D service, we'll set up a firewall to block all non
HTTP
traffic. And we'll even set up SSL so that our application can handle HTTPS traffic. But
after
that, we'll also take a look at how we can deploy our application onto Heroku. Just because if
you don't have, you know, maybe you can't afford to pay for cloud services, or you just don't have
the
ability to sign up for an account or something like that, I do want to make sure that you
still have a way of deploying your applications that you can show off to your friends and family what
you
created. So I added the Heroku section, because they've got a very, very nice and convenient
free
tier where we can deploy our entire application for free, we don't need to sign up with the
credit card. So that's why I included that second deployment scenario. And since all the
cool
kids are hardcore into Docker today, I'm going to show you how to Dockerize your API in case
that is your preferred method of deployment. And then finally, we're going to wrap things up by
building
out our very own CI CD pipeline using GitHub actions. This will allow us to push out
changes
to GitHub, resulting in our pipeline running, which will pull our code, run all of our
integration tests, build all of the necessary images. And if all the tests ultimately pass, it'll actually
push
out our changes to our production environment, so that we can do all of this in an automated
fashion without having to manually go in and run each step manually. So let's take a look at our tech
stack.
Since this is a Python API course, we will be using Python to build out our API. There were a
couple of different web frameworks in Python that we could have used most notably Django
and
flask, I decided to use neither one of them. And I decided to use a newer framework called
fast API.
And the reason why I chose to use this framework was because it has API's kind of built in
mind,
right? It wasn't there to address like the model view controller type scenario, it really is
all about building out API's. And on top of that, it is really fast, right? But when I say
fast,
it's not just fast from a performance perspective, which it is. But it's also fast from the
fact that it can, it makes it really easy and quick to spin up new API's. And one of my favorite
features
of this framework is the auto documentation functionality. When you build an API, you
have
to document how your API works. And this is a very cumbersome task. Because anytime you make
any changes to your API, you have to remember to update your API, or then the front end can
be
making the wrong request. Fast API automatically documents your API for you so that you don't
have to do it yourself. It's truly a game changer. And finally, the most important reason why I
chose
fast API is because of this. You see this magnificent beast? This is the creator of
the
fast API framework. And my mom always told me when someone with a mustache as glorious as
that
creates a web framework, you use that damn framework. Now, as I mentioned, we will be
covering
SQL extensively. And I've decided to go with Postgres. It doesn't really matter what type of
SQL
database you use. They're all fundamentally the same with only minor differences. I chose
Postgres because that's my favorite. And who doesn't like elephants anyways, for our
ORM,
when we do eventually migrate to ORMs from using raw SQL queries, we will be using SQL
alchemy.
I decided to use that because that seems like that's the most standard one for Python. I
really could have chose any of them. I didn't really care which one I used. So I just picked the most
popular
one. So that covers our tech stack. Let's now take a look at the project that we'll be
building.
Now I would love to show you some cool flashy website that you can show off to your friends.
However, we're not building a website, we're building an API. And unfortunately,
API's don't really have a visual aspect to them. So I don't really have anything to show you.
I mean, I could I guess construct a couple of HTTP packets that send it and verify that we get
the
proper JSON response. But that's not really exciting. So I'm going to do is I'm going to show
you our documentation for our project so that you can see all the different features that
we
implement. And the nice part about the documentation is these are interactive docs. So from
the documentation, I can actually send HTTP requests to my application and get a response back. So
you
can really see all of the different features, things like the authentication and the crowd
operations that we'll be performing and setting up in our API. So the app that we're going
to
build for this course is going to be a social media type application where users can create
posts, they can read other people's posts, they'll be able to perform all the crowd operations.
So
they're able to create, read, update, and delete posts. And we'll also be able to vote on
posts.
So you know, most social media apps have some sort of like system or voting system. And so we
will
be able to like other users posts as well. So this is the built in documentation that comes
with fast
API. And you'll be able to see all of the API endpoints that we're going to create in this
course. So we're going to have all of the posts endpoint, which is going to be responsible for
retrieving
all posts, creating posts, retrieving an individual post, updating a post as well as deleting
a post.
Then we'll have the user specific endpoints of things like creating a user getting a user's
information. And then we've got our authentication, which is going to be used for logging in. And
then
finally, we've got the one for voting. So when you want to vote on a specific, a specific post
that
you like, you can go ahead and like that post. So let's just use the documentation to
actually,
you know, test out our API. So first things first, let's actually try to retrieve any or all
of our
posts. So if I hit this Get button right here, we can actually try to send a request to our
API with the built in documentation. So you can just say execute, right, and you'll see that we got
a
401, which means that we are right now not logged in. So we are unauthorized to retrieve any
of the
posts. So the API that we're going to build is going to require all users to be logged in to
be to even be able to read the posts. So let's actually go ahead and create a user. And
so
we'll go down to the user's endpoint under Create user. And I'm going to do try it out. And
it's
going to give us an example of the structure of the data that we have to send to our API to
create a user. So we need to provide a username or an email and a password.
So I'm going to create an email, I'll call this john at gmail.com. And the password is going
to
be something simple, just for demonstration purposes. And we'll execute that, right? So
it'll
actually send that data back to our back end, you can see we get a 201, which means we were
successfully able to create it, you can see the ID of the user account that was created, we can see the
email is
what it is, and then we can see the date that it was created at as well. And so now we can
actually log in as this user. So there's a couple of different ways to log in, I can go to the
login
endpoint, or we can just go up here at the top and just do authorize. And then we can provide
that information. So I'll do john at gmail.com. And we'll provide his password. And if we
do
authorize now, we can see that we have successfully logged in. So I'll close this out. And so
now if
I try to retrieve the posts, and then hit execute, you can see that we were able to retrieve
all of
the posts in our database right now, there's only two. But you can see that these were created
by some other users like Sanjeev in this case, and then Sanjeev 123. And if we want to create
our
own posts, we can go ahead and do that. So I'll go to the create post endpoint. And we'll do
try it
out. And it's going to give us an example here. So we have to provide title content, as well
as is this post going to be published, or is it going to be a draft. And you'll actually see the
structure
of this schema right down here. I think it's at the bottom, actually. So if you go to
post,
create here, this is going to show us the the schema that we have to pass, we have to provide
a title content. So the asterisk means it's required, the published is not required.
And
you can see that it defaults to true. So if we don't provide a value, it'll default to true.
So let's test that out. And so I'll just say favorite foods is going to be the title.
And the content is going to be pizza and burgers. And I will remove the
published
section because it is optional and no default to true anyways. And we'll hit execute. And if
we
take a look at the response, we get a 201, which means it successfully created it. And you can
see what the post looks like in our database. You've got the title content, we can see the
published
got defaulted to true, we could see the ID of the post when it was created, we could see the
owner
ID, which is, you know, who created the post. And so that's the idea of our account, which is
17. And we can see the information about the owner. So this is john at gmail.com.
And then, you know, as usual, we could perform all the other CRUD operations. So we can
retrieve an individual post, update it and delete it. And then once again, if we go down here, we can
also
vote to like a post as well. And so, you know, this kind of forms the backbone of a
traditional
social media type application. So once you can really do this, you can really create any
application you want. And I think this covers enough things from an API perspective, as
well
as from a SQL and database perspective, that you will have a solid foundation to really build
out any API that you're interested in building out. And so I think that's enough talking, let's get
to
actually start coding out our application. In this video, we'll take a look at how to set up
Python,
as well as VS code on a Mac machine. The first thing we're going to do is we're going to just
search for Python. And the first result should take us to the main Python page. And what we
want
to do is like downloads, it's automatically going to give us a button to download whatever the
latest version of Python is. And the nice part is it automatically detect what platform you're
using,
so it'll know that we're on a Mac. And right now, it says that the latest version of 3.9.6. So
it's going to offer me that keep in mind, if you're watching this video in the future, it's going
to
show a different version. And it's okay for you to download a newer version, as long as it's
later than 3.7, you should be okay for this course. So I'm going to select this and it's going to
download
it like any other application. Once it's finished downloading, we're gonna select that and
then select the file that we just downloaded. And then this is going to take us through the
Python
installation process. So we'll hit Continue, we'll hit Continue again, continue. We'll hit
Agree,
and then install. And then it's going to prompt us for a password.
Alright, and so once you get this pop up, it means that Python was successfully installed. So
we can then close that out real quick. And then we can move this to trash. And the next
thing
that we want to do is go up to your search bar at the top. And I want you to search for the
terminal
application. And you should find that just double click on that. So we can open up a
terminal.
And with our terminal, what we want to do is we want to type in Python, three, dash, dash
version.
And the reason we specify Python three is that you can actually have a Python two and a
Python
three version simultaneously installed simultaneously on your machine, we want to make sure
that we got Python three successfully installed. So if we type this in, it should then print out the version
that
we installed. So we can see that we did successfully install Python three dot nine dot six. So
we should be set from a Python perspective. And now that we have Python installed, let's go ahead and
install
VS code. So like I said, VS code is going to act as our text editor or ID. And so what we're
going
to do is just search for VS code. And this is going to take us to the Microsoft page. And
then
once again, Microsoft will automatically detect that we're running a Mac. And then we could
just go ahead and select download Mac universal. Alright, and once that's done installing
or
downloading, we can select that and open it up. And you'll get this warning, just go ahead
and
select open. And so this is going to open up our VS code. And what we need to do is VS code is
just
a very basic text editor. However, it comes with extensions that we can install that make
it
significantly more feature rich. So it'll almost act like a traditional ID. And if you select
the
extension icon, which is this icon right here with the extra blocks, select that and I want
you to search for Python. And the first result should be the one you want and it should be the one
that's
made by Microsoft. So this is going to provide things like linting, IntelliSense debugging and
a few other features that are going to come in handy when it comes to writing Python code
within
VS code. So go ahead and install this. Alright, so now that it's installed, what we want to do
is
select this icon to go back to our folder menu. And we want to select open folder. So what
we're going to do is we're going to create a folder to store all of our project code, I'm going to
select
open folder. Now we haven't created a folder to actually store our application. So I'm going
to
go under my traditional users, Sanjeev, and then I'm just going to store this within my
documents,
I'm going to make a new folder called, I'll call this, we'll just call this fast
API.
Well, create that and then we're going to open up the fast API folder that we just
created.
And so this is going to open up all of our project code. Right now we have no files, and
that's to be expected. But what I want to do is I want to right click here and select new
file.
And we want to do is type in main.py. And before we hit enter, I want you to look down
here.
But when I hit enter, you'll see that something's happening. So it's activating the Python
extension. And it's automatically going to selected Python interpreter. And so like I
said,
you can have multiple different versions of Python. So technically, we could, you know, for
any one of our projects, we can be running three dot nine dot six, or maybe we're
running
two dot seven, if we want to stick to Python two, we can actually select which specific Python
interpreter we want to run on a per project basis. And if you ever want to change
this,
or for some reason, it wasn't able to find this correctly, just go ahead and select view,
command palette. And then it automatically found the most recently used ones, but you would
just
search for Python, select interpreter, select this. And it shows the currently selected
interpreter,
but you can also enter in the path to find a specific interpreter. So you would just find
wherever Python two dot seven was installed. If you wanted to use that, or if it was unable
to
find three dot nine dot six, go ahead and just find where Python three dot nine to six was
installed on your machine and point to that Python dot exe file. Actually, it's not called dot
exe
within Mac, I forget what it's called on a Mac, I actually use a Windows machine for most of
my things. But that's all you have to do. And so at this point, we've got Python setup. And then
we've
got our VS code setup. So once again, anytime VS code closes, you want to open it back up and
then
open up the folder fast API in this case. In this video, I'm going to walk you through setting
up
Python as well as VS code on a Windows machine. So the first thing that we're going to do is
within
your web browser, just search for Python. And then we're going to select the first result. So
this is going to take us to the main Python page, we want to go to the download
section.
And so here, Python, the website's going to offer you up whatever the latest version of Python
is. In this case, it happens to be three dot nine dot six. Keep in mind, if you're watching this
video
in the future, it's going to show some other version. And that's okay, go ahead and just
download the latest version. As long as it's a later than three dot seven, you should be
okay.
So I'm going to just hit download Python three dot nine at six. I'm going to download that
once it's finished downloading, I'm going to select that and it's going to open up the
installer.
Now with this installer open, it's very important that you select add Python three dot nine
to
path. Do not forget to do this, or then you're going to run into some issues. So make sure
that's selected and then hit install. Now, if you go through the installation process, and you
realize
you forgot to do this, technically, there's a way to fix it afterwards. But instead of me
explaining to do that, the best thing to do is just uninstall Python and reinstall it. So hit install.
Now,
you'll get a pop up, just go ahead and hit Yes. All right. And once you see this message where
it says set up was successful, that means Python was installed. And the next thing that we want
to
do is just quickly verify that Python is up and running and working. So what we're gonna do is
we're just going to search for CMD. So it's going to open up our command prompt or terminal.
And
then in here, just type in Python, dash three, dash dash version. Sorry, we should actually
type
in Python py dash three, dash dash version. Alright, and so when you type this command
in,
it's going to tell us the specific version that we installed. So we can see that I was
successfully installed Python three dot nine dot six. If you get any kind of error, or any message saying
that
this command is not available, that means that Python wasn't installed successfully. So just
go ahead and redo that process just in case. Now, the next thing that we want to do is install VS
code.
So let's open up a new tab, just search for VS code. And then we'll select this is going to
take
us to this page. And you'll see that VS code will automatically detect what version you're
running. So you can just like download for Windows. And then once it's finished in
downloading,
go ahead and open it up at the accept agreement at next, next, next, and then you can leave
everything
as default. And then go ahead and run install. Alright, and then go ahead and finish it's
going
to automatically open up VS code. Now there's a couple of things I want to do. So VS code at
its
core is just a basic text editor. However, we can install extensions that give it extra
functionality,
extra features that will make it operate a little bit more like a full blown ID. So go ahead
and
select this icon right here. This is for extensions. And then what we want to do is we want to
search for Python. And we usually just want to select the first one, it'll have a star and it's gonna be
the
one made by Microsoft, you'll see that there's other ones made by other users, but we want the
main Microsoft one, hit install. And so this extension is going to give us
IntelliSense,
linting, debugging, and a few other features that are going to make it a little bit easier to
start working with Python within VS code. Alright, and so now that it's done installing, we can
just
close this, close this, and then select this one to get back to your main file browser. And
right
now we haven't opened up any folder. So what we want to do is we want to create a folder to
store all of our application code. So select open folder, and then figure out where you want
to
store your application code. So I'm just gonna store this in my documents, you can store this
wherever you want. I'm gonna create a new file or a new folder, I'm just gonna call it fast
API.
But you can name your project whatever you want. So just select folder. And so that's going to
not
have only create not only create the folder, but it's going to open it up within VS code. If
you get this error, just select Trust the author, and just like yes. And then we can close out
this
welcome menu. And so now what we have is we've opened up whoops, we've opened up VS code.
And
within VS code, we've opened up our project code. So right now there's no code that's to be
expected. But what I want to do is I want to create a new file real quick, just for testing
purposes.
So I'm just gonna right click hit new file, and I'm going to name it name
main.py.
And so we want to name it.py, because there's gonna be a Python file. And before I hit enter,
I want you to focus your eyes right down here. And you'll notice something happens down
there.
So when I hit enter, you'll see activating extension. So it's activating the Python extension.
And what the Python extension is going to do is it's automatically going to select the
correct
Python interpreter, or it may not be correct, depending on what happens on your machine. For
me, it automatically detected Python three dot nine dot six was installed on the machine.
So
let's selected that one for me. Keep in mind, if you have multiple versions of Python
installed, it may select the wrong interpreter. So what you can do is if you ever want to change
the
interpreter or specify a specific interpreter, go to view command palette. And what I want you
to
do is search for Python interpreter, and then look for a Python select interpreter. So right
now,
it's going to highlight where the current Python interpreters which one selected and what's
the executable for it. But if you want to change that you can select enter interpreter path and
then
provide the path to the specific version that you want. So you can always change it. But
usually it's able to detect the latest version. So you should be good to go. And so that's all I
wanted
to do from a setup perspective. There's one more thing that we'll cover later. But we'll get
to that in the next section. But right now we've got Python installed and we've got VS code set
up.
And we've got our project directory setup, so we should be good to go. So now that we have
Python
and VS code setup, the next thing that we have to do is set up our virtual environment. Before
we do that, we have to actually talk about what our virtual environments and what problem do they
try
to address. So let me give you a little bit of an example scenario of the issue that we can
run
into when working with specific packages within Python on your local machine. So let's say
that
we created a project and this project is called project one. And what we need to do is we need
to install fast API version one dot two dot one. So what do we do, we install version one dot
two
dot one on our machine so that we can actually use that version. Now let's say later down the
road,
we start a new project called project two. And let's say this version requires us to run
fast
API version two dot four dot three, because we want to try out some of the newer features.
Well, at this point, if we need to try out a newer version, we have to upgrade fast API to
version
two dot four dot three on our local machine. And this may or may not be a problem, it really
just depends on if version two dot four dot three is backwards compatible with version one dot two
dot
one. Because if version two dot four dot three has breaking changes, then ultimately, that's
going
to create issues with our project one because that project expects us to run version one dot
two dot one. And so if we can upgrade, well, we're in a little bit of a pickle because one project
needs
one version, the other project needs another version, we can have two different versions
installed on our machine. So what exactly do we do? Well, this is where virtual environments
come
into play. So with virtual environments, let's say we want to create a project. So we
create
project one. And what we do is we create a virtual environment, call it whatever you want. And
so this is a isolated environment that will not affect any other environments. And within this
environment,
we can install any Python packages running whatever version we want. And it's completely
isolated to
this project. And so when we create a second project, what we can do is we can create its very
own virtual environment as well, we'll call it virtual environment two. And within this
virtual
environment, we can install any version of any package that we want. So then we can install
version two dot four dot three. And so both of these virtual environments are completely
separated,
completely isolated with one another. And so we can essentially install multiple different
versions of a single package for each of our projects. And that way, our projects don't end up stepping
on
the toes of other projects. And so now that we have a basic understanding of virtual
environments, let's go ahead and figure out how we can actually create our first virtual environment and use
it
within our project. In this video, I'm going to walk you through setting up a virtual
environment
on a Windows machine. Now, if you already close out your VS code window, go ahead and open up
a new one and make sure you open up the specific directory we created a couple lessons ago.
Now,
what we want to do is we want to select terminal, and we want to select new terminal. And this
is a awesome feature with VS code, it allows us to have a integrated terminal within our VS
code
window. And this terminal is fundamentally no different than a regular terminal within
Windows, it's just built into VS code so that we don't have to keep flipping between different
windows.
And if you actually take a look at this right column, we can have multiple terminals open and
we can even use different terminals. So the default one on this machine happens to be
PowerShell.
But you can also use command prompt if that's what you prefer. So if you select command
prompt, it's going to create a new terminal. And you can see that this is a traditional command
prompt,
I'm going to use this one because that's the one I normally use. However, if you're more
comfortable with PowerShell, feel free to use that. Now the command to actually create a virtual
environment
within Windows is p y dash three dash m b n v. So that stands for virtual environment. And
then you
want to give it a name. So give whatever name you want for your virtual environment, you can
name it after your project name, or you could just do what I do, which is just call it V and V. And what
I
like to do is for all of my projects, I give it the same exact name. And that's perfectly
okay, because this virtual environment is going to be isolated to this project directory. And so
even
though this project has a virtual environment in V and V, I can create another virtual
environment in any other project with the same exact name, they're all going to be isolated to that
specific
project folder. So go ahead and hit enter. And take a look over here, you'll see that a
folder
was created called via virtual environment or V and V. And that's going to be the name of
this
folder is always going to refer to whatever name you gave it. So if I named this cookies, this
would open up a new folder called cookies. And if you want, you can take a look at
what's
in here. And if you go under scripts specifically, you'll notice that we've got our Python
executable. So this is going to act as our new interpreter. And so what we want to do is right now we
are
using our global interpreter. And we want to change that we don't want to use the global
one,
we want to use the specific one in our virtual environment. And that way we can install
packages
that are exclusive to just this specific virtual environment. And so to do that, go ahead and
select view once again. And we want to go to command palette, and then search for
Python
select interpreter, as usual. And then you'll see the one we're currently using, but we want
to pass
in our newest one. So select enter interpreter path. And then we want to give it the path
to
our interpreter, which is the path from the root of our project directory to this Python dot
exe
file, we're going to say dot, which is our current folder. So this is going to be the fast API
folder.
And then within here, we want to go into scripts. And then within scripts, well, sorry, we
want to
go into the virtual envy folder, then scripts, and then Python dot exe, go ahead and hit
enter.
And then if you look down here, you'll see that this gets updated. So now we're still using
three dot nine dot six, but we're using our virtual environment. And so that's all we need to
do.
And it should remember this every time we open up our project, however, double check every
time you
open up just to make sure you're using that if it's for some reason moved back to the default
one, just do the same process. Now, the second thing that we need to do is we need to make sure
our
terminal is also using the virtual environment. And so what we want to do to do that is just
type in
the path to this specific activate dot bat file, this is going to activate that virtual
environment
within our command line as well. So I'm going to say virtual envy. And then we want to pass
in
our scripts folder, and then activate dot that hit enter. And then notice what's changed,
right.
So when you're using a virtual environment, right, you're going to see the name of the virtual
environment to the side of it. So always double check whenever you start up VS code, and you
start
up your project, that this is enabled, if you don't see this, then just run the same exact
command. And that's going to reactivate the virtual environment. And so at this point, we are
good
to go with our virtual environment. And we can start coding out our project. In this
video,
I'm going to walk you through setting up a virtual environment on a Mac machine. Now, if
you've already closed out your VS code window, go ahead and open it back up and open up
the
folder that we created a couple lessons ago. And within here, what we want to do is we want
to
access our terminal. So we can access our terminal just by selecting this. And then this is
our terminal. However, VS code has a feature where we can access the terminal directly within our
VS
code window. So if we select terminal and the new terminal, that's going to create a new
terminal for us. All right. And so this is fundamentally the same thing as this terminal right
here,
it's no different. But at least by having it built into VS code, we don't have to keep
flipping back and forth between the two. So you can use either one, whichever your
preferences,
keep in mind that when you open up the terminal within VS code, it automatically moves you to
the the directory folder of your project. So you don't have to move there yourself.
You can use whichever one you want. Now, I'm actually using I'm running Mac on a virtual
machine right now, just because I natively use a Windows machine. So I don't actually own a
Mac.
So this is a little bit buggy. And if you see me type things, you can see it vanish. So for
demonstration purposes, I'm just going to use the regular terminal, but I strongly recommend you
use
the one built into VS code. So you don't have to keep flipping back and forth. Now to
actually
create a virtual environment, there's a one command that we have to run. So you run
Python,
three dash M, V, N, V for virtual environment, and then you have to give it a name. So what
name do
you want to give your virtual environment? You can technically give it any name you want. You
can name it after your project, you can give it some arbitrary name. What I like to do is I like
to
name all of my virtual environments, V, N, V. And so for every project that I create, I just
create
the same exact virtual environment name, V, N, V. And that's not going to create an issue.
It's perfectly okay to name all of your virtual environments the same thing, because it's
all
going to be exclusive to this specific project folder. So no other projects can access it. So
it's okay, if they all use the same name, it's just easier. And so that way, you know, if I
ever
create a get ignore file, I can automatically add V, N, V. And I can make sure to just use the
same
exact get ignore template across all of my Python projects. So I'm going to use V, N, V. And I
want
to make sure that you pay attention right here. Because after I run this command, take a look
at what happens. It creates a folder called V, N, V. And that's based off of the name of the
virtual
environment. And if we take a look at our virtual environment, you can see a couple of
different files, you can see the, the Python file as well. So if you go under bin, this is going to be
the
new Python file or the Python three file that we're going to use for our interpreter. So down
here
at the bottom, you'll see that we are still using the global interpreter, we no longer want to
use the global one, we want to use the one in our virtual environment. That way, we don't, you
know,
cause any issues with other projects, we want to use the virtual environment. So to do that,
what we want to do is we want to select a view, command palette. And then we want to do
the
Python select interpreter again. So we'll select that. Right. And so right now, it shows us
that
we are using the global one, however, we're going to pass in the path to our specific
interpreter
within our virtual environment. So we'll select enter interpreter path. We'll say dot for
current
directory. And so that's going to mean the current fast API directory, then within there, we
want to
go into the virtual environment folder. And then within there, we want to go into bin. And
then
just say Python. And so we'll hit enter. And then take a look at what happens down
here.
Right, you notice that the version still says three dot nine dot six, but our interpreter is
actually pointing to our local virtual environment now, because we changed the interpreter to
use
this specific Python file right here. And so this is what's going to allow us to install our
own
specific fast API version, as well as any other version of any package that we want. So it
doesn't actually install it globally on our machine. Now, we're almost done. But there's one other thing
to
note. If you look at our terminal, our terminal isn't using our virtual environment. So if I
run pip install any package, it's not going to install it for a specific virtual environment, it's
going
to install globally. So we have to enable the virtual environment for the command line as
well.
And to do that, all you have to do is type in source. And then we want to pass in the path
to
this specific activate file right here within bin. So we do the env slash bin slash
activate,
hit enter. And then notice how our command line changes. Now it's prepended by the name of
our
virtual environment. So since we named it v env, you can see that it shows it right before the
terminal. And so that's all you have to do. Our virtual environment is set up, keep in
mind,
if you close out your terminal, and then reopen it, you'll notice that your virtual
environment
is gone. So you have to run that same exact command every time you open up your terminal to
ensure that you are in your virtual environment. And anytime you close out VS code, make sure
that
when you reopen your project, it still points to the virtual environment. If for some reason
it changes, then just go back to that command palette, by going to view command palette, and
then
selecting the interpreter that you specifically want. But that's all we have to do guys. So
our virtual environment set up. And so at this point, we can go ahead and get started on coding out
our
project. But now that we got our environment set up, it's time to get started coding
out
our project. And what I want you guys to do is first of all, go to the fast API
documentation,
this is really important. What I want you guys to do is really get a solid understanding of
how the documentation works. So pull up the fast API website, and then head on over to the
tutorial
section and then select the intro part. So this is going to walk us through setting up our
project. And what we will need to do is first of all, we need to install the fast API package. And so
what
we can do is we can either do pip install fast API, or we can do pip install fast API, all
when we do
pip install fast API, all it's going to install all of the optional dependencies as well,
which we may or may not need depending on what features we want, we're going to go ahead and use the
all
option because we're going to use a lot of them. And there's no point in having to go one by
one and install them. So that's what we're going to do. So here, in our command line, and make sure
that
we are in our virtual environment, and then just type in pip install fast API. And then we'll
pass
in all and we'll let that run. Now it's moving fairly fast. But if you kind of scroll
through
the history, you'll notice a lot of packages, and a lot of dependencies that are getting
installed. So we'll take a look at those once it's done, just to see what are the different
dependencies
that were installed. And so now that that's done, if we type in pip freeze, this is going to
show
all of the packages that were installed. So it's not going to be just fast API, it's going to
include a lot of the optional dependencies as well. So if we take a look, you can see that we've
got
graph ql installed, if we want to use any graph ql, we have B crypt UV coin, that's going to
be
like our web server web sockets, we want to work with web sockets. So it already comes bundled
with a lot of things that we're going to use. And I just wanted to make sure that you guys
understood
what's happening when we pass in the that all flag. And if you want to, if you open up your
virtual environment folder, and you go under lib, you'll see all of the the code
associated
with those packages that we installed. So that's where all of them are going to reside,
they're all going to reside within the lib folder. Alright, so now that we've got fast API
installed,
the first thing that we have to do is we have to import fast API. And to import fast API, we
just say from fast API, which is the name of the library, we import fast API.
Alright, so we've now successfully imported fast API, let's create an instance of fast API.
So
we'll say app equals fast API. And then we'll call that function. Keep in mind, you can
name
this anything you want. But if you take a look at the documentation, and just follow along,
you can see that it's going to name an app. So I think it's best if we just kind of follow
along
with that convention, then what we're going to do is we're going to just copy this code right
here. So this is going to be what's referred to as a path operation. And we'll go over what that
means
in a bit. And we'll just paste this here. I want you to save your code, you'll see if you get
this
warning or this message saying format or auto pep eight is not installed, go ahead and hit
yes.
And so this is nice, because it will automatically format our code. So if we ever put too many
spaces or things like that, you'll see that as soon as you save it, it's going to snap into place and
make
everything look nice and pretty. So let's save this. And so once we've got our code, let's
actually start our web server up. So how do we actually start our web server? Well, once again,
let's
go to our documentation. So we're going to make use of the uvcoin library. So because we
installed
fast API with the all flag, it automatically installed uvcoin. If you didn't use the all flag,
then you'll have to do pip install uvcoin as well. But we already got it installed.
And so let's go back here. And let's run our API. So we do uvcoin. Then what we have to do is
we
have to reference the name of our file. So this is their entry point into our application,
which is our main file. So here, we're going to say main, keep in mind if this file was named
anything
else, if you don't have to name it main, you can name it anything else, you would just want to
pass the name of whatever the file name is, then you do colon, then you pass in the name of your
fast
API instance. So we named it app. And so we'll do app. And then we'll start that. Right. And
so this
is saying that we started our server. Perfect. And we could see the URL that our server is
running on. So it's going to say HTTP colon slash slash 127 dot zero, zero to one. So if you guys
don't
know what that address is, that means it's this machine. So whatever IP address, this machine
runs on, that's what this is going to be. So it's just saying a we want to refer to the local
host,
and it's going to run on port 8000. So if you want to, you could just copy this
URL,
go to your web browser, paste it in here, and then see what happens. And then look at
this,
I'm going to zoom in for you. It says message Hello World, and that's coming from our
code.
Right. And that's coming from this return statement right here. So that so this kind of
verifies that everything worked perfectly. Alright, so now that we've got everything up and
running
and working, and you can see that when we go to our website, we can see that we are properly
getting back our message. Let's pause this video. And in the next video, we'll take a look at
exactly
what each line of this code actually means. Alright guys, so let's take a look at the
code
that we added and actually dissect what each line means. So we've got these three lines of
code. And if you take a look at the fast API documentation that we were on, it's going to define those
three
lines of code as a path operation. So that's the terminology is using it refers to this as a
path operation. Now, the name itself doesn't really matter. You'll see that in other web
frameworks,
especially in other languages, sometimes they refer to this as a route. But they within
the
documentation for fast API refer to as a path operation. So you'll see me flip between those
two terms, but they fundamentally mean the same thing. So let's actually take a look at
this
specific path operation. And we can really see that it's made up of two components. The first
component is going to be the function. And the second thing is going to be the decorator.
So
we'll come back to the decorator in a bit. Let's take a look at this function. So this
function is fundamentally no different than any other Python function. It's a plain old
function,
you'll see that there's this async keyword, technically, this is optional. This keywords only
needed if you're going to be performing some sort of asynchronous tasks, so something
that
takes a certain amount of time. So things like, you know, making an API call, things like
talking to the database, if you want to do that asynchronously, you do have to pass in the async
keyword.
But we're not doing that right now. So what we can do is we can actually just remove that. So
I'm going to delete that, and just remove that. And so now it's just a regular function. And you'll
see
that the code behaves exactly the same. So we have a function, we give the function a name.
So
the documentation just shows an arbitrary name of root, keep in mind the name itself doesn't
matter. So if I wanted to name this, you know, get user, then that's going to be fine, it's not going
to
change anything, it's an arbitrary name. However, I do recommend you name your functions, your
path
operation functions to be as descriptive as possible. So if you're trying to log in a
user,
then maybe you should call this login user, or just log in so that it's as descriptive as
possible.
But keep in mind, the name that we put here does not matter. So I'll change this back to root
for now. And then we have so then within this function, you can perform any kind of logic. So if
this
function is meant to log in a user, it's going to have all the code for logging in a user,
whatever that may be. So maybe you know, checking the passwords in a database to make sure
that
they match and to make sure that the credentials are properly accurate. And then after
that,
you know, just like any other function, we can return something. So whatever we return here is
going to be the data that gets sent back to the user. So if we go back to the website, go to
our
web server, take a look at this message, Hello, world, that's exactly what we sent. And if we
change this to be whatever we want, it's going to get returned back in the same way. So
here,
we're just returning a Python dictionary, I guess. And what happens is, fast API will
automatically
convert this to JSON, which is the main universal language of API, right? We all talk to we
use
JSONs to send data back and forth between an API. So it converts this JSON, and it sends it
back to
the user. And that's why we see that on the web browser. Now, the next thing that we have is
this
decorator, if you're not really familiar with decorators in Python, it's okay, you don't
really actually need to understand, you know, the core concept of a decorator, just understand that
when
you apply a decorator to a function, it's going to perform a little magic to this function.
Because if I remove this decorator, if we just comment it out, take a look at this code, this code
has
nothing to do with fast API, it's a plain old function. So how do we actually make it, you
know, act like an API? Well, we have to use this magical decorator, this decorator turns this into
an
actual path operation, so that someone who wants to use our API can hit this endpoint. And so
you
just specify at the at symbol, that's what the that's how we clarify that this is going to be
a decorator, then we reference our fast API instance. And then we have a couple of different
options.
So what here we pass in is the HTTP method that the user should use. So this is a get
method,
which means that we have to send a get request to our API. And, but we can use plenty of
different
HTTP methods. And I strongly recommend you actually take a look at the different HTTP methods.
If you
do HTTP methods, we can select this one, this is the Mozilla page, you can see all of the
different
HTTP methods. So there's get, post, put, delete. So those are the main ones. There's a couple
of
other ones that are sometimes used. But for the most part, those are the core ones. And so
here,
once again, just the HTTP method. And then finally, we have the path. And so this is the root
path.
And so, and so it's a little bit hard to explain the path, but it's basically the path after
the
specific domain name of your API. So if you take a look at our, our URL, our web server is
hosted
on this specific URL. If I go to this page, let's open up a new link, just paste it in here.
So,
so the URL and the path in this case is just slash. So that's equivalent of just hitting enter
right here, or putting a slash, which doesn't change anything, right? Right, whether
there's slashes there or not, it's basically going to take you to the same URL. It's very
similar to going to, you know, google.com. Right, that's going to take us to google.com. But if you
go
to google.com slash, it's, it's the same thing. So it's the root path. So whatever domain
name,
our API is hosted on, whatever URL is just saying it's the root path. You know, if I change
this to,
you know, log in, right, that means that this path operation will only apply if the
user
goes to our URL, and then goes to slash login. So that decorator, this path right here
just
references the path that we have to go to in the URL. And so if this is actually changed
to,
how about posts, and then you know, like vote, so maybe this is the URL for voting on a
specific
post, then we would have to go and do the same thing here. So we'd have to go to post
slash
vote. So nothing too complicated. But those are the two pieces that make up a simple path
operation,
you've got the function, then you've got this decorator, where you have to pass in the
specific HTTP method, and then the URL you want it to go to. And I'm going to change this back to the
default.
And now what I want to do is, let's go ahead and make a simple change. So what I'm going to do
is I'm going to change this, I'm going to change the message to be welcome to my API, we'll save
this,
go back to the root URL, because we changed it to the root URL. Let's hit refresh. Notice how
it still says Hello, world, nothing changed. So what gives, right,
it, you know, our codes changed, I saved it, why are we not updating it so that it returns
welcome to my API? Well, the problem is, is that anytime you make a change, we have to restart our
server.
So to restart our server, you hit Ctrl C, which is going to stop it, then you could just hit
the up arrow keys that you can find that command that you ran before, and just run it again. And so
now
if I hit refresh, we can see that it updated. And I'm sure you're thinking, well, that's a
little annoying. Every time I change my code, I have to do a Ctrl C, and then an up arrow and then
hit
Enter, just that I can, you know, make sure that the server actually implements those changes.
And it is a little annoying, but there is a workaround. So if we go to the
documentation,
you'll see that when they run uvcorn, they pass in the dash dash reload flag. And the dash
dash reload flag will actually take a look at your code and monitor your code. So anytime that you
change
your code, it'll automatically restart your server for you. So let's try that out. I'm going
to do a Ctrl C. I'm going to hit the up arrow, and I'm gonna pass in the dash dash reload flag. So
we'll
hit enter. And now what I want you to do is make some changes. So it doesn't really matter
what you
change, I'm just gonna add a couple of exclamation points. And then I'm going to hit save. And
I want you to focus what on what happens down here. So if I hit save, look at that, it
automatically
restarted the server for me. And so now if I go back to my website, or my API, hit refresh,
you can
see that the exclamation points are there. So moving forward, in a development environment,
only when we're in a development environment, we're going to pass in the dash dash reload
flag.
When we go to production, we don't need that, we're not going to be setting it up because
we're not going to be changing our code in a production environment. And so I think that's a
good
stopping point here. In the next video, we'll just quickly review exactly what is a path
operation,
so that we could just reinforce what we learned in this lecture. All right, guys, so let's
quickly
recap what we learned in the previous lecture, I want to make sure that you guys have a solid
understanding of the different components of a path operation, because ultimately, that's all
your
API is, it's just a bunch of path operations. So first things first, we have our decorator. So
our
decorator has the little at symbol. And so that's what signifies it as a decorator. And then
we
reference our fast API instance, which we called app. And then we have our HTTP method. So in
this
case, this is going to match only get methods. And then we have the specific path or the URL.
So this
is the root URL in this case. And then below that we've got our specific path operation
function. So
this function is going to contain all the logic for performing some kind of task. And when
it's going to return some data, and that's the data that gets returned to the user when they hit
this
specific path operation. So now that we have a solid understanding of how path operations
work,
let's see if we can create a new path operation. And let's say this one represents retrieving
a bunch of social media posts from our application. So in this case, you know, there's two things
that
we need. So the first thing is our function, our path operation function. So we'll say def.
And then we'll give this function any name, I think, since we're going to be retrieving a bunch
of
posts, I think a good name is going to be get underscore posts. But keep in mind, you can
name
this anything you want. Like I said, it does not impact the behavior of anything. And then we
want
to see. So here we would pass in all of our logic for retrieving posts, but we don't actually
have an application. So I'm just gonna say return. And then we'll just say, maybe, I don't know,
data.
And this is going to be, this is your post, but in reality, we would provide a list of posts.
So
let's save that. And then there's one last thing we have to pass in our decorator, like we did
before to actually make this turn into a special path operation function. So we'll say at and
we
reference the instance, the fast API instance, we'll do app dot, and then we have to figure
out
what HTTP method we want to use for this path operation. So when it comes to retrieving
data,
you usually use a get operation. And if you don't know which one to use, you can just go back
to
this page right here, and it's going to explain what each one's for. So if you select get,
it's going to explain when you would use a get, if you want to use a post, it's going to explain
when
you should use post. But I already know that for retrieving data, it's usually a get
operation. So
we're going to keep it as a get method. Now for the URL, I'm going to say, to retrieve a
post,
we want to go to the slash posts URL. So we'll save this. And then let's see if we can
retrieve
that data. So if I hit refresh here, it's going to say Hello World. But that's because if you
take
a look at this, we're at the root path. And that's going to match this specific path
operation.
And this path operations on slash posts. So if we go to slash posts,
and then hit enter, take a look at that, we have now got our data, which is our posts. So we
have
hit that second path operation that we just created. And it really is as simple as that you
just define a function, you define the HTTP method, and then the URL. Now there's one thing to
know,
and that is that, you know, the way that fast API works is that when anytime we send a request
to
our API server, it's going to actually go down the list of all of our path operations. And
then
it's going to find the first match. And as soon as it finds the first match, it's going to
stop running your code. So if I actually change this to just the slash URL, just like this one
above,
what do you think is going to happen, right? They both reference the get method, and they both
reference the same URL. So which one do you think is going to win? Well, let's take a
look.
If I do just the slash again, it says Hello World. So it looks like the first one, one. And
the
reason for that is that once again, fast API literally just goes to the code, and it looks for
the first match. So there's really only two criteria. In this case, what's the HTTP
method,
it's a get. So anytime you work in your browser, your browser is always going to send a get
method by default. And then what's the URL. So the URL is a slash URL. So it matches this one. And it
does
not continue past that. So it never runs this code. So it sends that back. Now, if I took
this,
copied it, and moved it to the top. What do you think is going to happen? Which one do you
think
is going to run? Well, let's hit refresh. Look at that. So that one ran. So the first path
operation
that matches is always going to be the one that runs simple as that. So the order does in fact
matter. However, if I change this to posts, and leave this at the top, right, if I hit
refresh,
we get Hello World. So what happened was fast API went down the list. And it received a
request
that let me just put some comments. So the rec request comes in. Sorry, that's not the way
to
comment in Python. request comes in with a get method. And it comes in with the, the URL is
going
to be slash. All right. And so these are the two things that it looks forward to match. So it
hits
the first path operation. So this is a get so those two match, and then it looks at the URL,
which is slash, but that does not match. So it skips past with this one. And then it goes to
the
next one, you know, does, and then checks to see if that matches. So then get matches, the
URL
matches. So then it returns Hello World. And that's why you return Hello World. And the reason
I wanted to highlight this is that the order in fact, does matter. So you have to keep that in mind.
And
it can impact the way your API ultimately works. I'm going to just take this move this to the
bottom.
But in this case, the order doesn't matter because they're hitting two different paths or
two
different URLs. Till now, we've been using our web browser to generate HTTP requests to test
our API.
And that's fine for now. However, once we start getting into more complex path operations
and
routes, so things that involve having to send a HTTP point or patch, or any of the other
methods
and having to send data to our API, it gets very complicated, because there's no way to
natively do that in the browser, without generating or building out a complete full front end
application.
And to test an API, you shouldn't have to build an entire front end application to do that,
that would be, you know, unmanageable, unscalable. And so there's a lot of different tools that
we
have, that we can use to test our API. And so one of these tools is called postman. If you go
to
postman.com slash downloads, you can download this app. So go ahead and just hit the download
button, it's going to automatically detect what your operating system is. But this is a
very
simple tool. It's just a tool that allows us to construct our own HTTP request. So we get to
specify the individual fields of an HTTP request. So we could specify, you know, what is the
HTTP
method? What's the URL? What are the headers that we're going to apply? What's the body? What
kind of data is that going to carry? Is it going to have any authorization headers? So all of
these
things we get to construct in a nice GUI so that we can test our API. So go ahead and download
this.
Now I've already got it downloaded. But once you've got it downloaded, open up the desktop
application. And you should see something like this, right? Everything should be
relatively
empty. And what we want to do is, if we want to create a new HTTP request, all we have to do
is
hit the plus button. And before I do that, what I'm going to do is, let's change this to
dark
theme. So themes, I think this looks a little bit better. Yeah, that looks a lot better. And
I'm
also going to zoom in for you guys. Let's make it a little bit more. There you go.
Hopefully,
that's nice and easy for you guys to see now. So what we want to do is let's create a new HTTP
request. So hit this plus button. And you can see that this provides us all of the fields that
we
need to actually create an HTTP request. So the first thing that we need to do is specify
what's the HTTP method. So if we go back to our code, right, we only have get requests. So whether
we
want to hit either one of these path operations, it's going to have to be a get request. So
we're
going to leave it as get but you can see we have all the other whoops, we have all of the
other options. Then we have to specify the URL. So this is no different than the URL that we went to
in
our browser. We just copy this URL. And we actually need the HTTP in there as well probably.
So if we
actually go back to our server, and see where we started it, and actually the best way to get
the
URL is just to stop it, start it. And then that's going to give us the URL. So we just needed
the
HTTP beforehand. And then we just paste that in there. Okay, and so if I leave this as
the,
you know, the root path, whether I include the slash, it doesn't really matter, actually, I
can just hit send. And you look at this, this is the result that we got back from our API
server.
So it says message Hello, world. And once again, you guys already know where that came from.
And
that came from this specific return statement. And so you can see that this is so much
easier
than using a web browser. Well, you guys may not notice how easy it is at this point. But once
we get to constructing more complex path operations, you'll see that it's so much easier to use
a
tool like this. And keep in mind, this isn't the only tool that does this, there are plenty of
other applications that also do this, this is just the one that I'm familiar with, and the one
that
I use, but I don't want to, you know, force any specific application down your throat, you
guys can use whichever one you want. So if you already use one that you prefer, feel free to stick
with
it. But before we wrap up this video, let's just test out our other specific path. So this
one's
going to reside on slash posts, we're going to try that and we'll do slash posts. We'll hit
send.
And look at that, we got our data that says this is your posts. So now we were successfully
able to test both of our path operations using postman. And moving forward for the rest of this
course,
we're no longer going to use the web browser, we're going to use postman to test out our
API.
Till now, we've only been working with get requests. So all of the HTTP requests
we've
sent to our API, all have been get requests. And so now we're going to learn about post
requests,
they're a little bit different. And I want to kind of highlight what makes post requests
different than get requests. Now, we're going to compare the two right now. And I've drawn out a
little
diagram for us. So at the left side, we've got our web browser, or it could be our mobile
device,
depending on whatever the front end is. And on the right side, we've got our API server. So
this is our fast API server. Now, what they get requests, what we do is we just send an HTTP get
requests.
And then we send that to our API server. And then our API server sends back some kind of data
depending on what they're trying to get. So if they're trying to get a whole bunch of
posts,
we send back the posts. Now with a post request, it's going to be similar, except there's
one
minor difference. And that is that with a post request, we can send data to the API
server.
So in this case, a lot of times what you'd use a post request for is for creating things. So
if I want to create a post, I would, when I say post, I mean, like a social media post, not post as
in
post request. But if I wanted to create a social media post, I would send an HTTP post
request.
And I would include all of the data needed to create a post on my API server. So we'll
send
that data over to the API server. And the API server will send back whatever data it thinks it
needs to send. And so in real life, if we were trying to create a post, you we would
include
whatever data we needed for posts. So in this case, we would include what's the title of the
post, what's the content of the post, what's the user of the post, we send that to the API
server,
the API server can then talk to whatever database to actually create the post. And then it can
send back some data, you know, something like, hey, I've successfully created the post. And then
here's
the final post after I created and then send that the send those details back to your web
browser
or your mobile devices. So think of it like this, a get request is basically saying, Hey, API
server,
give me some data. Whereas a post request is saying, hey, API server, here's some data, do
whatever you need to do with it. So it's like, so it really controls the direction
of flow of data between the front end and the API server. So get requests are getting data
from the
API server, whereas a post request is sending data to the API server. So now that we have
a
basic understanding of how post requests work, let's go back to our code and see if we can
create
a path operation for a post request. So let's go below our last path operation, which was
forgetting
posts. And let's create one for creating posts. So first things first, let's, let's just
actually create our decorator. First, we'll do app dot. And instead of guest, sorry, instead of
get,
we're gonna say post. So that's all it takes to convert a traditional get request to a post
request, you just say dot post. And then once again, we're gonna say whatever specific URL
the
user should go to, to actually create the post. Now, I'm going to do something a little bit
bad, I'm going to say the path that we want to go to is called create posts. And if you've ever
worked
with API, this is kind of going against best practice. But don't worry, we'll correct this in
the next lesson. Because there are certain best practices is not going to break our application
by
any means. We'll say this, and then we'll say def. And then here, we'll just name our
function, I'll just say create post, like I said, the name of the function never
matters.
And then here, which all we're gonna do is we're going to return, we'll say message, oops,
message.
And then we'll say successfully created posts. Let's save that. And let's go to our
postman.
And so we've got already one request. So I could just change this to post. And then we have
to
change the URL in this case. So we'll go back and see that create posts. So it's going to be
a
create posts. And let's hit send. And let's see what happens. Look at that message
successfully
created. So we have successfully sent a post request to our API. Now, one thing I like to
do
with postman is that you can create multiple requests and then have them saved. So I'm
going
to actually change this back to just go to posts. And then go to get and then we'll keep that.
And
what we can do is we can just add another request. And so I'm just going to copy the URL here.
I'm going to paste this. And then this is actually going to be create posts. And then this could
be
a post request. And so now we can hit a get request and then hit a post request
really
quickly, because they're essentially, we've got them in two different windows. And let's
just
quickly double check that still works. That's good. And then let's double check get posts that
still works perfect. Now, that's cool. And all, however, the whole idea behind a post request
is
to send some data to our API server. So how do we do that? Well, let's go to our post
requests.
And what we want to do is we want to send some data in the body of the request. And to do
that,
within postman, you would go to the body section right here. And then we can do that. And
then
within body, what we want to do is go to raw, and then select the type. And normally, when
you're
working with API's, you want to use JSON, however, you can use a XML and a few others,
however,
most people use JSON. So we'll select JSON. And then we'll do and JSON looks very, it
operates
very similarly to a Python dictionary. So it's, you know, curly braces, and then it's going to
be
a whole bunch of key value pairs. So here, I'll say, what's the title of my post?
Well,
title, my post is going to be say, I will say, top beaches in Florida.
And then the content of the post is going to say, check out these awesome beaches,
beaches.
All right. And so now, if we hit send, let's see what happens. Great. So it says
successfully
create post, we successfully hit this endpoint. But in our path operation, how do we
actually
extract the data that we sent in the body? How do we retrieve that body data? Well, what we
can do
is within our path operation function, I can say, I can just assign it some variable. So
what
variable do we want to store all that body data, you can pick any name you want. I'm just
gonna say this represents payload, although it could be something like body or anything else. So
we'll
say payload. And then what we want to do is colon. And then we want to say, it's going to be
of a type
dict. And we want to set this equal to body. And we want to import this. So this is
actually
something that comes from the fast API library. So if you do tab, if you hit tab, it's going
to
automatically import it, or you can select it. So you hit body, and then do that, and then
triple
dots. And just to keep in mind, right, if you see that from fast API params, it imported body.
So
what this is going to do is, it's going to extract all of the fields from the body, it's going
to
basically convert it to a Python dictionary. And it's going to store it inside a variable
named
payload. Pretty simple. And all we have to do is let's say print. Let's just print out
payload.
Now if we hit that post request, again, look at this, right here, we can see that it
converted
into a Python dictionary, and we extracted the title field, as well as the content field.
And
so that's how we extract the data from the body of the payload. And just to quickly recap,
once again,
we imported body from fast API dot params. And we let VS code do it automatically for
us,
I always recommend letting VS code do it for you, so that you don't have to memorize. And
remember,
you know, where in the fast API library, this, this property is stored. So we're taking
this,
taking the body, and then we're converting it into a dictionary. And then we're storing it
inside a
property called payload, but you can name this whatever you want. And so what we can do is
we
can take this data. And then we can say, let's say we're going to return back a new new
post,
and then we're just going to send the data. So we'll say title, and we can actually change
this
to a f string. And we'll say title. And then we can pass in payload. And you'll see that we
can
just reference the title property because it's just a regular Python dictionary. And we'll
say
that the content is going to be payload content. So let's save that. And then now let's hit
send.
Now look at this, we got our new post innocent. Whoops, it looks like I made a mistake.
Yeah,
no, actually, that's correct. title, top beaches in Florida content, check out these awesome
beaches.
So that's pretty simple, guys, I showed you guys how to not only send data in the body within
a
postman request, we're also able to extract that data and send it right back to the user. Now
in
a real application, we would take the data, and then we would normally store that data
inside
our database so that we can create a new post stored in the database. And then now anytime the
user tries to retrieve the post, we can fetch that data from the database, we don't have
a
database set up just yet. So for now, I'm just showing you guys how to extract that data and
then send it back in the request. In the last lecture, we learned about how to work with
post
requests, we learned about how to send data in the body, using postman as well as how to
extract that data within our path operation so that we can perform some logic. And there's some issues
that
we're running into at the moment, based off of the way we've kind of set up things. Alright,
and
the first thing is, it's kind of hard to get all of those values from the body, we have to,
you know,
extract each one individually. On top of that, the client can send whatever data that it
wants. And
this is a big issue, right? I don't want the front end to send arbitrary data. If the user is
trying to create a post, I want the title, I want the content, nothing else, right? I don't want
them
to send any extra data. And on top of that, the data itself isn't getting validated, right?
So
how do I ensure that the user is sending what I want, right? What if the user sends a blank
title,
I can't have a post with a blank title. So how can we validate that the data that the user
sends
is actually valid, right? And ultimately, what we want to do is we want to force the user
into
a schema that we can expect, right? That's the term that we always use the API schema, where
we want to define exactly what the data should look like, so that it's almost like a
contract between the front end and the back end saying, Hey, the back end sends a message to
the front end saying, Listen, I expect my data to look like this. If you don't send me the data
looks
exactly like this, I'm going to give you an error. And that's the way you want to work with
APIs, you want to explicitly define what the data should look like, so that the front end can send
you
exactly what you expect it. So let's see how we can do that using fast API. Now, what we're
going
to do is we're going to make use of a library called pedantic. Now, we've already got this
installed, because we use that all flag when we did pip install fast API. So if you actually
go
back to your code, and go to your lib folder, you should see pedantic somewhere in here. And
you can
see that pedantic is already installed. So we can make use of pedantic to define what our
schema should look like. So let me quickly show you how to work with pedantic. It's really simple.
But
keep in mind, technically, pedantic has nothing to do with fast API, it's its own complete
and
separate library that you can use with any of your Python applications. Fast API just makes
use of
it so that we can define a schema. So let's import pedantic. So let's say from
pedantic,
import, base model. And so if we go down to our, you know, our create
posts,
path operation, you know, ultimately, what we want to do is we want to tell the front end
what
a post or a new post should look like, what data do we expect? So let's figure out what data
we want for a specific post request. So two things that we want, we want a title, which is going
to
have, which is going to be, you know, a string, essentially. And then we want the content,
which
is the content of the post, which is going to be, you know, some sort of string, right. And so
we want the we expect the user to send both of those things. And then we don't really want
anything
else. We don't want the user to send any other piece of data, we just want these two things.
And we can put in any other pieces of data we want. So if we want the user to pass in something
like,
you know, what is the category of the post, we can include that, we can maybe include maybe
the
number of I mean, I don't know, you can really think of anything, maybe a Boolean that kind
of
represents, you know, is this a published post? Or do you want this to be saved as a draft? So
you
can construct it however you want. But we're just going to stick to title and content. So the
way
that this works is, I'm going to remove this comment right here. We're going to define a
class,
we'll call this class, and then we'll give it whatever name we want, it's going to represent
what a post should look like. So I'm just going to call this post. And then this is going
to
extend base model. So this is what makes it a special pydantic model, we just extend base
model.
And then here, we pass in the different properties for our post. And like I said, we want a
title.
And we also want a the content. Now, what we pass here is going to be what is this type of
data. So
if we go to this pydantic library, you'll see that there's the different field types. So we
can set things to be a Boolean, we can set it to be an int float string. So all of the common Python
types
list tuple, it's all available within pydantic. So we know what a ultimately what a title
should look
like. So what do you guys think should be the field type for the title property? Well, I
think
it makes sense. It should be a string because this looks like a string, it's got some text,
it's got some text, it probably should be a string. So let's set this to be string. And the
content
should be the same thing, it should also be set to string. And now what we can do is let's
take this
model and go down to create posts. Instead of extracting the payload, what I'm going to do
is
I'm going to reference that post pydantic model. And then I'm going to save this as a
variable
called post. Once again, they don't have to match. But since this will represent the post, I
can call this maybe new underscore post or something like that. And then, and then
reference
the post pydantic model. So what's going to happen is, because we pass this into our path
operation,
fast API is automatically going to validate the data that it receives from the client based
off
of this model. So it's going to check, hey, does it have a title? If so, is it a string? If it
if
the title doesn't exist, or if it's not a string, and maybe we pass an integer, then it's
going to throw an error, it's also going to check for us if contents available. If it is
available,
it's going to make sure that it's a string. And if contents not available, or if it's not a
string, it's going to throw an error. So it's doing all of the validation for us. And then we
can
it's defining the schema of what it should look like for the front end, what kind of data
should
the front end send to us. So we're going to remove all of this nonsense.
I'm just gonna for now just say data. I don't know, new post for now.
And now what I'm gonna do instead of print payload, we're going to say print new underscore
post. So let's save this. And then I'm gonna send a post request. Okay, so that updated
fine.
But let's take a look at what this printed out. So take a look at what you're printing out.
Look at that. It automatically extracted all of that data for us. So now we can access, you know, post
dot
title, and we can get the title of the post. And we can also access post dot content to get
the content of the post. And just to prove that to you guys, we'll say post dot title. So we'll
save
that will send a request. And now look at that look how easy it is to extract that data
because
it's already assigned to that new post variable. And we can just access each property based
off of
what the model is defined as. Now that's great and all. But let's check to see if it's
actually
performing all of that validation. So if I go to my postman, and I remove
title,
let's see what happens. I'll hit send. Look at this. Look at this, it's saying that the
inside
the body, there's a title field and it's saying a value error, it's missing. Now we didn't
tell
fast API just to send this, but it automatically does the validation for us and sends back an
error message. And then it sends back a status code. So this is the status code of the
message
it sends a 422, we can change that later, don't worry about that. But it's automatically
performing the validation. And that's what's awesome about this. And if we, if I add the title
back,
what was it top beaches in Florida? And instead, actually, I changed this to
be
a one or something. And I need that comma. Let's see what happens if I hit send. It looks
like
there's no errors. And that's because the number one can actually be converted into a string.
So
it's not going to throw an error on that. And that's perfectly fine. And I'll and that's
perfectly
fine. So that's not a big deal. But let's change this back. Like I said, it's going to try
to
convert whatever data we give it to a string. So as long as it's able to convert it to a
string, it's okay in any integer that we pass can be converted to a string. So I'm going to
change
this back and I'll add top beaches in Florida. Florida. Let's see what happens if we remove
the
content now. I hit send. Look at that. Now it's saying the content is missing. Perfect. So
our
validation is working exactly how we expect it to. And then I forget what what was it
something
something beaches. I can't remember what it was. Now let's say that we want
to
assign a property. That's optional, right? Like what if we want it to make it so that the
front
end can either choose to send some piece of data or not send a piece of data. So let's say
that
we want the user to be able to define if a property if a post should be published or
not,
well, we can create a field called published, which is going to be set, which is gonna be a
type of
Boolean. And we can say, you know, first of all, we can say, if the user doesn't provide us a
value,
we can give it a default value. So here I'll say true. And so if the user doesn't provide
published,
then it's going to default to true. If it does provide published, then whatever value you give
it, that's what we're going to use it as. And I'm going to go down here. And we're going to
change
this to we're going to grab the published value. And then down here, we'll say published,
whoops,
can be set to true. So let's hit send. And we can see that it got set to true. That's
good.
If we set this to false, it's send, we can see that it's false. And then if we remove this
all
together, move that comma as well, hit send. Look at that, it defaults to true. So now we've
created
an app an optional field for our schema. So the user doesn't have to provide this, and it's
going
to default to true in this case. Now, let's say we wanted to create another field. However,
instead
of giving it a default value, if it's not sent, we want it to default to none. So we want it
to be
completely optional. And we're not going to store any value. And if the user doesn't provide
it, what we can do is let's say the field is a rating, let's say the user can give each post a
rating,
what we can say is optional. And we're going to have to import optional from typing from
the
typing library. So import optional. And then we have to pass in the type. So what is it going
to
be? In this case, a rating, I think an integer makes sense. And then we can say equals to
none.
So this is going to be a fully optional field. And if the user doesn't provide it, it's going
to default to a value of none. And so here, I'm just going to get the the value of our
rating. And so if I hit send right now, in our body, we don't have a rating field, I hit
send,
you can see that it defaults to a value of none, because we set this to be optional, none.
However,
if I pass in a rating field, I need to give it a value, some kind of value. So I'll say we'll
give
this maybe like four stars or something. I hit send, we can see that now look, the value of
rating
is set to four. And then finally, the last thing I want to note is because we set the type to
be an
integer, so even though it's optional, we still have to specify the type. If I give this a
value of a string, so you know, something like Hello, which is not a valid rating, take a look at
what
happens. Look at that. It says in the body, the rating field, the value is not a valid
integer.
So it wasn't able to convert whatever value we sent into integer. And so that's why it's
throwing
an error and saying like, this does not match up with schema, and it's throwing an error. And
so that's what's awesome about this is now we can ensure that our front end is sending the
exact
data that we expect by using these pedantic models. So moving forward in this
course,
we are going to really make use of pedantic models to ensure that the schemas not only
receiving
data from the front end, but also sending data back is all matching up with our organized
schema.
And actually, there's one last thing I want to show you guys. And that is that when we
actually
kind of extract that data and save it into new post, it actually stores it as a pedantic
model.
So it's a specific pedantic model. And each pedantic model has a method called dot
dict.
So if I do, if I print new post below this, what we can do is if you ever need to
convert
your pedantic model to a dictionary, all we have to do is write the name of the
variable,
we say dot dict. This is going to take that pedantic model, and then it's going to
convert
it to a dictionary. So let's print that out. Let's save it. Let's hit send.
Whoops,
and I forgot to, it's still throwing a validation error, but that's to be expected. So we'll
say change that to four. All right, take a look at the two different things. So this is a
pedantic
model that's just printing out the different properties of that of that model, whereas this is
a regular Python dictionary. So we can just send back a dictionary if we want to. So we
do
return new post and save that now if I hit send, we're just sending back a dictionary. And so
now
we send back the data with all of the different properties. And so you'll see that this is
actually a nice little handy tool to be able to convert it to a dictionary, which is something that we'll
be
needing to do in some of the future lectures. So I wanted to make sure we just cover our bases
real quick on that. And also, I don't like calling this new post, I'm just going to change this to
post
for now, because I think that's a little bit better, my opinion, because it is just a
post,
we don't need to know that it's a new post, it clearly is. And let's just double check that
everything works, hit send. Perfect. In this lesson,
we're going to talk about what a CRUD application is, as well as what our standard conventions
when it comes to creating an API for a CRUD based application. So CRUD is an acronym that
represents
the four main functions of an application. So any application, regardless of what it is, needs
to be
able to create things. So in our case, since we're building a, you know, a social media
type
application, we need to be able to create posts make new posts, we need to be able to read
posts.
And so that includes, you know, retrieving all of the pre existing posts, we need to be able
to update a post if we want to implement that functionality. So if we decide to change
what
our post says, we would use the update functionality. And then finally, we need to be able to
delete a post. And so the CRUD that represents the four main functions of any CRUD based application.
So
it's nothing more than an acronym. However, I created this slide so that you guys can
understand what our standard conventions when it comes to creating an API for a CRUD based
application,
because there are certain best practices that we need to follow. And so when it comes to
naming
the URL and the paths for each operation, there's a standard convention. And the first thing
that I
want to point out is since we're working with posts, it makes sense to name all of the URLs,
all the paths with slash posts. And it's important that you use the plural form of posts, you
don't
want to do slash post, you want to do slash posts as in plural, this is standard convention
for
API's. And if we were working with users, and we want to do be able to create, read, update
and delete users, it wouldn't be slash user, it would be slash users. So always use the plural
that
standard convention. Now when it comes to creating a post, right, that's always going to be a
post
request. So we've got the create functionality right here. And we can see that we have to send
a post request standard convention, anytime you want to create an entity, it's going to be a
post
request. And the URL or the path for that specific request is always going to be slash posts
in this
case. And if you want to see what that looks like, for the decorator and fast API, it's pretty
simple, you just do app dot post slash post. So this is something you guys already know. Now, when
it
comes to read functionality, which would be, you know, reading or retrieving pre existing
posts, there's actually two different path operations we're going to create. So the first one is
going
to be slash posts. And this is going to be for retrieving either all of the posts or
multiple
posts, depending on what filter that we use. And so when it comes to retrieving information
from a
database or anything like that, retrieving data, it's always going to be a get operation. So
you send a get request to slash posts. And if you want to see what that face to the decorator on
fast
API would look like, it's just app dot get slash posts. Once again, we already have this set
up in our application so far. However, there's also going to be another path operation for
reading,
right? And that's if you want to get one individual post. So if we want to get detailed
information about one specific post, we're going to send a get request to slash posts slash
and
then we've got this ID right here. So anytime you create something, what's going to happen
is
anytime you add something to a database, the database is going to give it that specific item,
a unique identifier so that we can uniquely identify that specific entry. And so if I
want
to get detailed information about a specific post, I would just send a request to slash
posts,
and then pass the ID of that specific post I'm interested in. So that's what this ID
represents.
And within fast API, if you want to see what that actually looks like, you would just do app
dot get slash posts. And then you do curly braces, and then ID and that'll allow you to
extract
the ID from the specific URL of the request. And then we can, you know, take that
URL,
sorry, take that ID, and then you know, send it out to our database so that we can retrieve
the information. So there's always going to be two specific path operations for read
functionality.
Then we have update. So this would involve updating a pre existing post. So maybe
we,
you know, posted out something, and then we realized we said something offensive, and we want
to update it before anyone takes a screenshot of it. Now, when it comes to updating,
right, there's two different HTTP methods that we can use, we can use put or patch. And it's
really a matter of user preference, the only difference is that I want to I don't want to spend too
much
time on it is that when we use put that you the idea is that you pass all of the
information
all of the information for updating it. So all of the fields have to be sent to the to the
API
server, whereas a patch, we can just send the specific field that we want to change. So
put,
you want to change the entire thing, you have to pass every, every field, even if it's going
to be the same, and most fields don't change. Whereas a patch, we would just change the one
specific
field. So as an example, as a, you know, if we're working with users, actually, if we're
working with posts, and let's say I wanted to change the title, all right, I want to update the title
of
a post, if I used a put request, I'd have to give the new title, and I would have to give the
pre existing content, right? Because the put, the idea behind put is that you have to provide
all
of the same information, so that we can on the back end, just take all the information and
then update the entity. Whereas a patch operation, I can just send the title, and then my back
end
should know how to just update that one field, we're going to stick with put I believe, in
our
application, but really, at the end of the day, it doesn't really matter. It's just a matter
of use of preference. But with update, just like when it comes to retrieving one specific post, we have
to
pass in the ID so that we know which specific post we want to update. And for the decorator
and fast
API, the only thing that we change is it's going to be app dot put. And then we can pass in
the ID
like we did before. And then finally, we have delete. So if you want to delete a post, once
again, you're going to send a delete request, so that the HTTP verb is going to be delete. And then we
have
to pass in the specific ID of the post as well. And then this is what the fast API decorator
is
going to look like. So it's actually not that hard. It's fairly simple. And you'll see that
once you create one crud API, creating another one is almost a matter of copying and pasting
really.
And so in the next video, we're going to make a few changes to our API so that we can make
sure that we follow this naming convention. Because right now, I believe when it comes to creating
a
post, instead of just using slash posts, we call it slash create posts, which once again is
not following best practice. So we'll update that in the next video. Alright guys, so in the
last
lesson, we learned about best practice and naming conventions for an API. So what I want to do
is quickly go into all the code that we have and make changes so that we follow best practices. And
so
if you take a look at our get requests, you can see we say send a get request slash posts.
That's
perfect. We don't need to change anything to that. However, the issue is, is our create post
functionality. So the path for creating posts, we can see that we send it to a URL of slash
create
posts, that is not going and following best practice. So what we do just change this to be
slash posts.
And that's all we have to do. And so we'll save this. And we'll just double check inside our
post
man just to make sure we update that as well. So this is going to be to slash posts. And then
we'll just test this out to make sure we didn't break anything. And it looks like it's still
working
just fine. Alright guys, so if we take a look at our code, you can see that for our create
post
path function, we're not actually doing anything with the data, we're not actually saving the
post anywhere right now, we just print it out and then send it back to the user. So that's not
exactly
how a real application would work. And so what I want to do is I want to start saving these
posts, I want to start turning this into a real fully functioning application. Now, ideally, we're
going
to save this within a database, because that's how any application works, you want to take a
post, you want to save it in database so that you can persist that data. However, we're not quite
ready
to handle working with the database, a little complex, we're going to get to it in this
course, however, I want to keep things simple. So we're going to do is we're going to save the posts
in
memory. And how do we do that easy, I'm just going to create a variable, just
globally,
that's going to store all of our posts. So I'm going to say my underscore
posts.
And this is just going to be an array. And so this array is going to contain a whole bunch of
posts objects, right. And what each post is going to be is going to it's going to be essentially
a
dictionary. And the dictionary is going to look kind of like this. So we're going to have a
couple properties. So we already know that our post is going to have a title. And then that'll
have,
you know, whatever, you know, title of post one or whatever. And then we're going to have the
content,
of course. And I'll just say content of post one, this is just an example post. And then what
we'll
also do is, like I mentioned before, anytime you save an applicant, save a piece of
information
within a database, the database is going to create a unique identifier and ID. Now, since
we're not
working with databases, there's no ID, however, it's going to create issues. Because, you
know,
if you remember, when it when it comes to working with CRUD based API is we need to be able to
fetch
data and update data based off of the ID of a specific post. So we do need to still have an ID
so that we can uniquely reference any single item within this array. So we're going to
have
another field called ID. And then this is just going to have, you know, be some random
integer,
you know, 12345. It doesn't matter. It's just got to be unique. That's all that matters. So
this is what our post is going to look like. And we're going to just store it in this array. So
we're
going to have multiple posts. And I'm going to keep this one hard coded in here. Because every
time we change our code, and hit Save and refresh, guess what, it's going to clear this out.
Because
remember, this is just stored in memory. So every time our application restarts, we're going
to lose that data. So just to keep things simple, I'm going to hardcode another another entry. So
for
this post, we'll say the title is favorite foods. And the content will be I like pizza. And
we'll
just give this an ID of two. All right. And so now that we actually have some place to store
our
posts, we can actually test out our get post functionality to see if it works. So I'm
going
to save this, we're going to go back to postman. So find the one with the get operation where
we're retrieving the posts. And then we'll hit send. Let's see. Well, when we hit send,
nothing
happens, of course, because we actually have to update the code to send that data. So let's go
back to our code. And let's go to our get posts. So right now, we're just sending back
this
information. So let's update this so that we return my posts. And this should be
fairly
straightforward. What we can do is we can just keep this property called data. And we can just
remove this and just pass along my underscore posts. And so what's going to happen is fast
API
is great. Because if I pass in an array like this, it'll automatically serialize it. So it's
going to convert it into JSON, JSON has something that's also very similar to an array. And it's going
to
change that into a JSON format so that we can send it over our API. So that's all we have to
do, we just have to pass in the array, and it's going to send that. Now, if we try this, look at
that,
so we get the data property. And within here, we have an array. So this is JSON also has a
concept
of arrays. And then within here, you can see we've got our post one and our posts two. So
that's
really as simple as how to work with our API how to actually retrieve posts. In the next
video, we're going to update the create post path operation so that we can figure out how to
add
a new post into our my posts array. All right, guys, so let's update our create post
path
operation function so that we can retrieve the title and the content from our front
end,
and then create a brand new post and store it within our my posts array. So how do we do
that?
Well, we already know that we can retrieve our post by referencing this post
variable,
because this will take our schema, which remember, we defined with pedantic, it's going to do
all the
validation, and it's going to store it within post. And so this is still going to be a
pedantic model. However, our our array is going to be an array of dictionaries. So we have to convert that
to
dictionaries. And we already know that we can convert any pedantic model to a dictionary by
doing dot dict. And then that'll turn it into a standard Python dictionary. And at that
point,
it's pretty simple. This is just standard Python. At this point, we can just do a my
underscore posts dot append. And then we can append post dot dict, just like we did before, we don't we
can
remove these print statements, they're just cluttering things up. However, there's one
little
issue. Like I said, we need to have an ID for every entry. And normally, the database
handles
that. But since we're not actually working with a real database, we're going to have to do
this in software. So what I'm going to do is we're just going to assign it a random integer, not
really
that reliable. But if we pick a random number between one and, you know, 10 million, the odds
of us hitting the same number twice is almost next to none. So what we're going to do is we're
going
to use the random package. So we'll say from random import, ran range. And this is what
we're
going to use to create a random number, or a random integer. And so instead of doing
this,
I'm actually going to remove this for a second. And I spelled append wrong as well. What
we're
going to do is I'm going to say, post underscore dict, which is going to be the post
pedantic
model converted to a dictionary. I'm going to set that equal to post dot dict. Okay, so this
is
going to be the dictionary. And so now that we have a regular Python dictionary, what we can
say is post underscore dict. And then I'm going to reference the ID field that I'm creating. And
I'm
going to assign that to be a random number. So we're going to give it a range of zero to and
then
just pick some really, really, really, really large number. So this is going to ensure that
pretty much any entry won't it for development purposes is going to be unique. We'll append that to
the
array. And then here's the thing when it comes to how a usual API works is that when a front
end
sends the data to create a new post, after we create the post stored in our database, we
should send back the newly created post, including the brand new ID. So let's send that back. So in
this
case, I'm going to send back post underscore dict, because that's going to be the brand new
post
that we add to the specific add to our posts array. So let's try this out. Let's go to our
post
request that we have. And let's see all of our data. So we've got a couple of things title
content
rating, I don't really care about the rating. But I'll just leave it in there for now. It
shouldn't matter. And then we'll just hit send. And let's see what happens. And it looks like we got
an
error. So let's see what happened. All right, it looks like there's an and I already see
what
happened. I forgot to actually include post underscore dict. We actually have to pass in what
we want to append to the array. Let's save that. Try this again. Hopefully no errors.
Look
at that, guys. So it looks like we got back the newly created entry. And we can see that it
has
an ID of whatever that number is. And if we do a get request now to retrieve all of our
posts,
we should see that newly created entry. So let's take a look. But we've got post one post
two.
And then we've got that brand new post. Look at that, guys. So we are a little bit less
than
halfway done with creating our CRUD based application. Now, before moving any
further
and writing any more code, what I want to do is I want to actually save these requests,
because right now they're just kind of stored in memory. If we close out postman, it'll
still
remember it. But what we can do is we can create a collection. So here, I'm going to select
create collection. And I'm going to give this a name. So I'm just going to name this after my
project.
So I can call this, you know, fast API course, or, you know, since this is social media
app,
we can call it whatever we were to call it, I'm just gonna call it fast API course. We'll save
that. And then what I want to do is I want to save these two requests, so that I don't have
to
remember this in case they actually get deleted. So I'll hit save, save as, we'll save it. And
then
you can give it any name. So this is going to be, I forget, this is the get request. So I'm
going
to say I'm gonna call this get posts, and then store it inside this collection. And so now
within
this collection, you can see this request is stored in there. I'm gonna do the same thing for
the post request. So this is for creating posts, we'll do save as again, I'm going to say,
create
post. And then it's already remembers the most recent collection, but go ahead and select
the
collection if it didn't remember that. And then we'll hit save. And so now they're both saved
in here. And we can easily bring these up anytime even if we close out all of our requests. So
I
closed everything out, and bring that right back up, it's going to remember everything. And
then same thing with the post request, I go to the body, it even remembers the fields. So
Postman,
one of my favorite tools, when it comes to working with API's, and you know, as this course
goes along, you're going to continue to learn more and more things about Postman. Okay, guys,
let's
continue building out our CRUD application, the next function I want to implement is
retrieving one individual post. And so let's define a function for that. So here, I'll call this get
post,
keep in mind that this is singular. So I call this get post, whereas to retrieve all posts,
it's get posts. But like I said, the name of these functions don't actually matter. It's just
more
for for you more for readability. And the decorator is going to look like this. And so this is
based
off of that slide I already showed you. So it's going to be a get method. And then here, the
URL is going to be slash posts. Id, right, because the user is going to provide us the ID of
the
specific post that they're interested in. And so that's going to get embedded in the URL. So
they'll
send a get request to slash post slash and then whatever ID so if they want to see the
information for post one, they'll pass in a value of one. And what this is actually referred to the
proper
terminology for this is that this is called a path parameter. So this ID field represents a
path
parameter. And this parameter happens to represent the ID of a specific post, the user's
interest in seeing. And so what we can do is fast API will automatically extract this ID. And then we
can
pass it right into our function. So now our function has access to whatever value was in that
URL right there. And so if we want to, I can do a quick print of ID. And then we'll just
return
some hard coded data. So I'll just say data, as we'll call this, how about post underscore
detail,
I'll say this is the post you're interested in. Or even better, I can do is we'll change this
to
an f string. And I'll just say here is post. And then we can just pass an ID. So let's save
that
will go to postman. And what we want to do is we want to create a new post, but let's create
it in our collection. So I'm going to go in there, select Add request, give it whatever name you
want,
I'll say, get one post. And then for the URL, just copy the get posts URL. And then we want to
pass
in the ID that we're interested in. So we just do slash. And then in this case, I'm going to
get the post with an ID of two. And the reason why I'm doing that is because we've hard coded
one
of our posts have an ID of two. So I know that's always going to be there. And then we can
just try sending this and see what happens. So it says post detail, here is post, and then post
number
two. So it looks like we were successfully able to extract the ID, the path parameter that
was
passed into the specific URL that the user sent a request to. So we now have access to that at
that
point, we can just find whichever entry in my posts has that specific ID. Now we have to
implement
the logic for actually getting that post. And so there's many different ways of doing this.
And I'm telling you right now, this is probably not the best way of doing this. I'm not really
even
a Python expert, I'm sure there's much better ways of grabbing this information. However, keep
in mind the code is going to be changed later on once we start working with the database.
So
this at this point is just basic Python logic. And I do want to keep you I do want you guys to
keep
in mind that this may not be best practice for the best way of retrieving an individual post,
but I'm just going to create a simple function. And it's going to be called find post. So
this
is how you find a post by an ID. So the so we'll pass in an ID in this function to retrieve
the post. And what we'll do is we'll iterate over the my posts, we'll say for P in my
posts.
And we'll say if P so P represents the specific post that we're iterating over. So if this
specific post has an ID, which equals the ID that was passed into the function,
then we'll return P or return that specific post. And then at this point, we can call
this
function. So I'm going to remove that print statement and say post equals find underscore
post
ID. And then for this, we can just remove this and just return post. Let's save
this,
let's go to back to postman. And let's try this out. And it looks like I got an error. So
something
happened. And let's actually take a look at our code. Well, guys, I think I found the issue.
So
the reason why we keep getting none, right, if I keep sending things send, it says none. And
what
I did was I just printed out the post to see what we got. And it actually just prints out
none. So for some reason, we're unable to find posts. And I think I know exactly why. And so what I'm
going
to do is I'm going to do a print of ID again. And this is going to teach us an important
lesson.
So send another request. And it prints out two. However, what we're going to do
is,
I'm actually going to get the type of ID. So I forget how to do that. I think it's the
type,
I think you can just do type in Python. So I'm going to figure out what is the actual type
of
ID. And now if I send this request again, right, you could see that the type is actually a
string
and not an integer. And so when we pass this check in where we say if P ID, so if we get the
ID
field and compare it to the ID that we pass in, it's never going to equal that because one's
an integer, the other one's a string, they're never going to be equal. So the first thing that
we
actually have to do is before we call this function, we have to convert it to an integer. So I
can just say, int, that's going to convert it to an int.
Save that. And at this point, if I send this, it should now work. Look at that. So keep in
mind,
guys, anytime we have a path parameter, it's always going to be returned as a string, even if
it represents an integer or a number, we always have to manually convert it ourselves.
So don't forget to do that. Now, there is one little issue with what we're doing. So
everything's
working. And if I actually change this to a one, we should get a different post. So
everything's
working great. However, what happens if I type this? Well, let's hit send. Well, we get an
error,
right? Because we're trying to convert this to an int. And it's going to throw an error. And
then instead of sending back a nice, you know, built in response, we just get an internal server
error.
And the user has no idea what exactly is wrong. So we want to provide it feedback. So how can
we perform some kind of validation to ensure that whatever data that's passed into this
path
parameter can actually be properly converted to an integer? Well, we can perform validation
with
fast API. So here, for ID, what we can do is we can say I want this to be an integer. So what
it's going to do is it's first of all going to validate that it can be converted to an integer. And
then
it'll automatically convert it to an integer for us. So we no longer have to convert this
ourselves.
So here, I could just pass an ID because this will make sure that it's already converted. And
so now, if I save this, and I change this back to a number, first, let's make sure
that
everything's working. So it automatically converted it to an integer. And now if I pass in,
you know, a string, it's now going to throw an error, it's gonna say for a path parameter of ID, you can
see
that this value is not a valid integer. And so now the front end has a good way of
understanding
what they did wrong. And, you know, this can be anything, right? So if you wanted this to be
a
string, right, we can have this automatically get validated as a string and convert it into a
string, so that if they do actually send a string, no errors, right. And then if they tried to
actually,
even if you put in a number, it's not going to throw an error, because any number can actually
be converted into a string. But we want an integer. So I'm going to change this back to an
integer.
And then we'll just code this as two. Perfect. Make sure you save your request. And so now
we've now
implemented our third function within our CRUD app. So we've just got two more, we got to
handle
updating and deleting posts. But we've got all getting all posts, creating a post and then
getting
an individual post. And you'll see that for the last two, it's pretty much the same thing,
right, we just have to define our specific decorator, and then define the logic for actually
creating
and deleting a post from that array or updating a post as well. Now, before we move on, I do
want
to show you guys one potential issue that you could run into if you don't quite understand how
these routes work and how order matters within fast API. So what I'm going to do is I'm
just
going to create a dummy route and you guys don't need to follow along on this is just a quick
little demonstration. And I'm just going to make this another get request or handler for a
get
request. And then this one's going to be posts slash latest. So what this will do is when the
user
sends a request, a get request to this specific path, I use path, we're going to grab whatever
the latest post is. And so here, I'll just say def get latest post. And so all I'm going to
do
is I'm going to reference our my posts list. And I'll say we'll get the length of my
post.
And then we'll just minus one. So this will grab the latest post. And then what we're going to
do
is just return that here, we'll say detail. And then we'll pass in and actually, let me
store
this in a variable post equals and then we'll return post. We'll save this and I'm going
to
show you what happened. So let's create a new request. I'll just copy the URL from the
previous
one. Then here, we're going to change this to latest. Let's see what happens.
Interesting,
we get an error. So what exactly happened? Well, let's take a look at the error. It's saying
that
we have a path parameter called ID. And it looks like it's saying that this is not a valid
integer.
So I'm not even sure why it's referencing this error. I guess if we take a look at our
code,
right, there's, there's no path parameter here. So what is exactly happening? Well, let's take
a look, right? Our request looks like this. So it's going to slash posts slash latest.
So when we send this request, what's going to happen is that we're going to start all the way
at the top. And fast API is going to go through the list of all of our all of our paths. And
it's
going to find the first match. So we have one at just the root URL. So it's not going to
match
that. We have one at slash posts, it's once again, not going to match that. This one is for a
post
request. So it's not going to match that because we sent a get request. But then something
interesting happens here when we get to this get request, right? So this handles a get request, and then
it
matches slash posts, and then some variable. So technically, that variable could be
latest,
you see the issue that we're running into, because there's no way for fast API to know that
that route was meant for that that request was meant for this specific path, right?
Because
it does technically match on this. And then what happens is since it matches on this route, it
then tries to perform validation on ID, which is in this case, it's going to end up being
latest.
And then it's going to throw an error because latest can't be converted to an integer. And so
this is the example of where order matters. So you have to be careful when you structure
your
API, especially with your with your paths and your URLs, you want to make sure that you don't
run into this type of issue. So you know, one thing in this case, you could do is just move this
up,
that would fix the issue. Because now if you save this, and then run this, right, it
works.
And if you try to reference a specific ID, that's going to work as well. Well, in that
case,
let's try one. Yep. And so that works as well. Because slash posts slash one will never
match
this one, it'll only match this one. So be very careful when it comes to the order, it always
works
top down. So anytime you're working with path parameters, it could potentially result in you
guys accidentally matching other routes by accident. So you know, there's different ways
to
get around this, like I said, you can just move this up. Or if you wanted to change the URL
altogether, so that this is like a slash post slash, I don't know, you know, just put in
another
keyword, something like a recent or something recent slash latest or something. Well,
actually,
even then that would, yeah. And so just keep. And so I just want to make sure you keep that in
mind when you're structuring your API, because anytime you have a path parameter, it could result in
you
accidentally matching requests that were meant for a different, for a different route. So I'm
just
going to delete this, this was merely just for demonstration purposes. And I just want to make
sure that you guys understand the fast API just looks at all of your paths and just works its
way
down the list till finds the first batch. For our get post path operation, specifically the
one
where we retrieved one specific post, we do run into a little bit of an issue. So if we go to
our
postman real quick, and send a request to get one post, but in this case, instead of doing one
or
two, what I want to do is I want you to put in the ID, put in an ID that doesn't exist. So we
only
have right now, posts with an ID of one and two, what happens if we send it with a ID of
five,
you can see what we get back, it says, null, right? And I don't like that, because it doesn't
really
give the front end any feedback as to what exactly is happening. So they don't actually know,
you
know, they don't know if some kind of error occurred, or if we were unable to retrieve this
information properly, or if there's a server error, or if this item doesn't ultimately exist. And
so
we need a way to accurately tell the front end that, hey, the ID that you're searching for
does not actually exist in our database. And the best way to do that is through a method that I'm
sure
you guys are already a little bit familiar with. So right here, I've got my GitHub page for
our
course. And you can see it's just GitHub, my username and then the name of the repo. But
if
I search for if I put in a whole bunch of letters afterward, what's going to happen is GitHub
is going to look for a repo with this name that doesn't exist. And let's see what happens.
It
gets a 404. And I'm sure you guys have seen a 404 error at least once before in your life.
So
that's a specific HTTP status code that represents that this item was not found though it does
not
exist. And HTTP status codes are important. We haven't really talked about it in this course
up at this point, but there's a lot of different status codes. And if you actually just
search
for HTTP status code, you can just grab one of the top links, I'm going to go take a look at
the
Mozilla one, and you'll see all of the different HTTP status codes. And you'll see when to use
them. Now, most of the time, you're going to see that we get a 200 response back, that's the
default
one, the fast API sends that usually means that everything's good, everything worked. That's
why it says okay. However, we do have other ones that we do also commonly use. So anytime you
create
something, so within your CRUD application for create, anytime you send a POST
request,
usually, after you create the entity, you send a 201 back instead of a traditional 200. So
there's
a lot of HTTP status codes, and we're going to cover all of those in future lectures. But the
ones we're interested in is for when we have an error. So we scroll down 400 usually means there's
some
kind of bad error, something happened. And most importantly, we want the 404 not found. So
the
server cannot find the requested resource. So that's exactly what we're looking for. Keep in
mind, there's also 500 status codes, which means you know, some kind of server error, server
error
or server failure. So what we're going to do is I'm going to go back to our code. And what we
need
to do is we need to manipulate the response. Because right now, we're getting a 200
back,
we're getting a status code of 200. If I send it, it says null, and then it just sends 200. So
that doesn't really give us a good idea as to what's happening. So how do we manipulate the
response?
Well, we have to, first of all, import the response from fast API. And then what we're going
to do is
we're just going to pass it in as a parameter into our path operation function. So I'll just
call
this stored in a variable called response. And so once you get access to the response
object,
we can tweak it however we want. So what I'm going to do is, after we search for post, if we
didn't find a post, I'm just going to do a quick check, and I say, if not post,
that means if we didn't find a post, we're going to do is we're going to set the response. And
then we're going to grab a property out for response called status code. So here we can tweak
the
status code of the response, I'm gonna say, hey, I want this to be a 404. All right, now let's
save
it. Now if I hit send, it still says post detail none, but take a look at the status code,
it's now
updated to 404. And if I change this to an ID that does exist, right, we get the post
properly,
and the status code changes to 200, which means everything is good. But one way of doing it,
however, there's a slightly better way of doing it, same concept. But instead of hard
coding,
the value or trying to remember it, what we can do is we can import from the fast API
library,
something called status. And so now instead of having to remember what to use, we could
just
type in status. And then it's going to show you all of the different HTTP codes. And so now
you
could just quickly just look through which one ever one sounds the most accurate. So we'll
grab the 404. And now you don't need to worry about putting hard coding in a number, we can
just
reference, you know, I guess it's an it's probably an enumerator, but I'm not really sure. So
it
doesn't really matter. And then that's all you have to do. So let's test this out again. So
200
good changes to five 404 perfect. But the next thing I want to do is I don't want to
return
null that looks ugly. So instead, I'd like to actually throw an error. So in this
case,
in this if statement, I'm just going to call a return that only is going to ever run if post
doesn't exist. And we'll just say, in this case, detail, or we'll say message, I'll just
say
message. f string, I'll say post with ID. And we'll pass in what was the ID, I'll say was not
found.
All right. And now if we try this, look at that, we get our error post with ID was not
found,
that matches up with the ID that we gave it. And we got our 404. Now this was a, this is one
solution.
However, I think this is a little bit sloppy. And there's a better way of doing it. Instead,
what we can do is we can rage raise an HTTP exception. So this is a built in exception
into
fast API, that'll automatically, you know, you can pass in what the specific error code you
want,
as well as the message. And so that way, you don't have to hard code all of this. And it just
makes everything look a lot simpler. So we'll go to the fast API up here. And we're going to import
something
called HTTP exception. And now we can delete all of this nonsense code. Actually, I'm just
going to
comment it for now, just so I can reference it in a second. And all we have to say is we'll
raise an exception. So we raise HTTP exception. And then we have to pass in two things. So first is the
status
code. So we'll set the status code to be status dot and then same thing, right? So far,
nothing's
really changed. And then here, we'll just pass in the value for detail, which is going to be
the
message that fast API automatically responds back with, and I'm going to pass in the same
exact F
string. And so now we've basically replaced this with just this one line right here, this
HTTP
exception, I think this looks a little bit nicer. I don't like having to do the set the
response, and then having to pass in, I don't like having to pass in the response into the path
operation
function, having to set a property, and then having to return my message, we can remove all of
this nonsense. And I can get rid of all of these comments. And I think this is just a little
bit
cleaner. And moving forward, we're going to be using these HTTP exceptions a lot. Because all
we have to do is just pass in what's the HTTP status code, and then the message we want to send
back
to the user. And then that's what's going to be sent to them. So now once again, we're going
to try this out. And look at that. Alright, so we got the 404. And we got the message, the
detailed
message, right, lot cleaner, a lot simpler. Now, before we move any further, and before we
wrap up
this video, I did mention that anytime we create something, we should send back a 201. So if
we go
back to our create post, and then we'll just create a new post again, you just send this, we
created
a new post, look at the status code that gets sent back, it's got it gets sent back at 200.
And that
is wrong. Remember, anytime we create an entity based off of that documentation, we should
send a 201. So how exactly do we send how do we change what the default status code is for a
specific
path operation? Well, what we can do is we'll go back to our code. And let's find our create
post.
And if you want to change the default status code, what we do is inside the inside the
decorator,
we'll pass in another option. So we'll say status code here. And then we set this to be status
and
then whatever our specific status code is. So I want to a one. And then that's all you have to
do. So now, by default, anytime someone sends a request to create posts, we're going to send
a
201 created. So we'll save that we'll give it another shot. If I try this again, now we get
a
201. Perfect. At this point, I think you guys will have a solid understanding of how to change
status
codes, whether it's because we're trying to throw an exception, or if we want to just change
the default status code of a specific path operation. All right, guys, so now it's time to move on
to
deleting a post. So to delete a post, remember, we're going to start off with our
decorator,
and it's going to request or it's going to require a delete HTTP request. Right. And then the
URL is
going to be the same. So it's going to be posts, and then we're going to need the ID. So we
can figure out which post to actually delete. Right, then let's define our function. So I'm just
going
to call this delete post. And so here, we have to implement the logic for deleting posts. So
what
are we going to do? Well, for me, there's many different ways, you know, deleting a post is
just
a matter of trying to figure out which specific dictionary within this array has a specific
ID
of whatever ID we give it, and then we just remove it from the array. So however you want to
do it, go ahead and do it. I'm going to just show you the first way that I thought of. Like I
said,
this doesn't actually have anything to do with fast API, this is more of just simply working
with Python. So I'm telling you right up front, this may not be the most efficient way to do
it.
But like I said, we're eventually going to implement a real database. So it doesn't really
matter how you do it as long as it works. And so the first thing that we're going to do is I'm
going
to look to find the index in the array that has required ID, we're going to look for what
we're
going to try to get the index of that specific item. And then all we're going to do is we'll
do a my underscore posts, dot pop, and then just pass in the index. And so that's how we remove it
from
that array. Pretty simple. And what I'm going to do is I'm going to actually just define a
function out here, just to keep things simple. So this is going to be called find underscore index
underscore
post. And then we're going to pass in the ID that we're interested in. And then we're going to
iterate over the array. But we're also going to grab the specific index as we iterate over it. And so
you
do that by using enumerate. So this is going to iterate over it, we'll get the access to
the
individual posts that we're iterating over, as well as the index. And so we'll say, if P, if
the ID
equals equals ID, and I forgot the n keyword here, we'll just return i. So this is going to
give us
the index of the dictionary with that specific ID. So here, we can just call that function.
And we'll
set it equal to the index. So index equals find underscore index of post pass an ID. So we
now
have the index. And at this point, I can just do a my posts dot pop, and then just pass in
index.
And then we'll try to do a return. And then here, I'm just going to pass in. We'll say
message.
We'll say post was successfully deleted. Let's give this a shot. So first of
all,
let's get all the posts. If I do a get all posts, you'll see that we have two posts. And then
I'm
going to create a new request. And this will be called delete post. Copy that URL, pass it
in
there. And so here, I'm going to try and delete with a post of a value of one. And I realized
I
still left as get so we're gonna change the delete. And so now it throws an error. So what
exactly
happened? Well, if we go back to here, and then go back to our console, it says, says non type
object
cannot be interpreted as an integer. And, and I realized I forgot to actually pass in
ID
into our function. So that's why it was giving us that error. But now let's try this
again.
And we see that we get the message says post was successfully deleted. That's good.
However,
you'll see that the status code is 200. And so if we actually go back to our status codes,
you'll
see that there's a specific status code that we should be using for when we delete something.
And
that status code is a 204. And like I said, you know, you can read the documentation, but this
is something that just, you know, comes with time of working with API. So you'll know that anytime
you
want to delete something, you want to use a 204. But before we actually do that, let's verify
that
it actually deleted it. So we'll go to get posts. And I'll send a request. And we can see that
there's only one post. So it clearly did work. But let's update the status code. So do you
guys
remember how to update the status code, the default status code of a specific path
operation,
we just pass in status code into the decorator. And then we'll do status. And then we'll just
get our 204. Now let's try to delete something again. We'll try to delete, where's my
delete?
We'll try to delete post one again, let's see what happens. So you'll see here that we get the
204. So everything looks good, we didn't get any data back. And if we go back to our
application,
you'll see that we got a an error, which says too much data for declared content length. And
what's
happening here is I actually had to look up this error. And that's because anytime you send
back a 204, the idea is that since we delete anything, we shouldn't really be sending any data
back.
And so when we try to send this message back, fast API essentially throws an error saying
like, Listen, you're sending a 204, we should not be sending anything back. So that's how they
kind
of implemented fast API. And so the what you should be doing is, if you want to do if
you
what we should do is actually just delete this, we should grab the response. And then just
pass
in status code status HTTP 204. Right? So basically, we don't want to actually send any data
back
anytime someone delete something, we just want to send a 204. And then one of the requirements
for that to work is that we don't send any data back. I know it's a little confusing. If you
actually
just Google this error, you'll probably get better explanation. But 204, when you delete
something,
you don't want to send any data back. That's kind of the expected result. So let's try this
again.
Let's send it. All right, so we got the 204, no data comes back. And no errors. So that's all
we
have to do. So this is just something special that you have to do for delete within fast API.
Just keep that in mind. It's more of just a copy of a cut and paste type scenario. However, before
we
wrap up this delete video, we are still running into one tiny issue. If I try to grab a an
ID,
that doesn't exist, let's see what happens. Well, now we get a 500 error. And we get an error
because
well, none type object cannot be interpreted as an integer. So since we tried to grab the
index,
from this find index post, it's going to return nothing because there's no post that has an
idea of five. So we get nothing. And then we try to pop with an index of non that creates an
error.
So we need to put in a simple if statement to actually catch that scenario. So we'll say if
index equals equals non, so return none, if it doesn't exist, what are we going to do?
Well,
we're going to raise an exception again. So we can use that raise HTTP exception. And we'll
just
say status code equals in this case, we want to send a 404 like we normally do. So we'll say
status
dot HTTP 404. And then we can send a detailed message. So in this case, you know, we'll do
once
again, another f string. And we'll say post with ID past the ID does not exist. Let's try this
out.
Perfect 404 post with ID doesn't exist. Let's just make sure we didn't break the original
functionality. We delete it. Perfect. And let's just double check there's no errors. There we
go.
So everything looks good to go, guys. So we'll wrap up this video. And then in the next
video,
we'll take a look at how we can update posts. All right, guys, we're almost done with all
of
our credit operations. The final one is update. So let's create a new request. We're gonna
call
this update post. And I'm just gonna grab the URL from the delete post, because it's gonna
follow
the same exact structure. So we're gonna pass the ID of the post that we want to
update.
Now, since we are updating it, we need our front end, or in this case, postman, to actually
send the data that we want to update with. So since we are working with posts,
and we take a look at our structure of our posts, it's very simple, we just have title
content,
and then, you know, published as well, if you want to add that in ratings as well. And I'm
actually going to remove that. So I don't actually want to have that I'll actually
will keep it in for now. But we'll definitely remove that a little bit later on. And so
if
I go back to postman, we can go to our body and then provide all of those fields. So we'll go
to raw, and then JSON. But this is just like when we were trying to create a post, we just
passed
that data in. And so here, we'll get the title. And we'll give it a new title. So updated
title.
So this, this, this update request is just going to update the title. Now, we are going to
use
the put method. So with put method, we have to actually pass in the data for all of the
fields,
right, we want to pass in what the final post will look like after we update it, we don't just
want to pass in the fields that we want to update. So if I go to let's say, you know, post one,
right,
we have the title, and then we have the content. So if I wanted to update just the title,
right,
I would make sure that I pass the content as well. So I'm just going to copy this. Since we're
not updating, I'm just going to send back the same exact content.
All right, and then this is going to be a put operation. And so this is what the request is
going to look like. Now, let's go to our code. And let's
actually create our path operation for that. So here, we're going to do app dot put is
going
to go to posts ID. And then we'll define our function, I'm gonna call this update post.
We're
gonna get the ID, and I'm gonna convert that to an int. And then one other thing, right,
because
we are receiving data from the front end, just like we did for creating a post, we want to
make
sure that it adheres to a very strict structure. So we get to use so I want to use our schema
again,
so that the front end can't just send whatever it wants to. Now, I can create a new model
called
class, you know, update post. And then we can pass in all the fields that we expect.
However,
it's going to be the same thing as the post because we expect the title content. And then any
of the other fields, depending on what we want, ultimately, but it's going to be a post.
So
it doesn't make sense to create a new schema, we can just use the pre existing
schema.
And so here, I'm just going to pass in post and make this type of post, so that it's going
to
make sure that the request comes in with the right schema. And then for now, I'm just
going
to hard code the return to be a message. And I'll just say, updated post. And then
here,
I'm going to do a print, let's just see what the post looks like. All right, so we were able
to
extract all of that data. So perfect. And then what we're going to do now is, there's a
couple
things. So first things first, I need to find the index that of the post with this specific
ID. So
just like we did with delete, I'm just going to copy this. Actually, we can copy all of this
right here. Because if that ID doesn't exist, we want to send a 404. So we can reuse all of that
logic.
But if we did find an index, that means we do need to update it. So what I'm going to do is
we're going to grab post underscore dict, which equals post dot dict, that's going to take
the
data we received from our front end, which is stored in post. And it's going to convert it to
a dictionary. That's all it's doing. And then what I want to do is I want to set post underscore
dict
of ID to be equal to ID. So we're going to set the ID inside this new dictionary to be that
ID.
And then we're going to say my underscore post. And we're going to pass in the index of
this
specific post we want to update and set that equal to post underscore dict. And then
lastly,
we're going to return, I'll say data. And we're going to return our new post, which is going
to
be post underscore dict. So let's just quickly recap what's happening. So first of all, the
user
is going to send a put request to the specific ID of the post he wants to update. And we're
going
to just do a quick check. So we're going to see if we can find what is the index of that
specific post
within my my posts, Ray. If it doesn't exist, then we're gonna throw a 404. If it does
exist,
whoops, if it doesn't exist, we're gonna throw a 404. If it does exist, well, first thing,
we're going to take all the data we received from the front end, which is stored in post, we're
going
to convert it to a regular Python dictionary. And then we're going to add the ID so that this
final dictionary has the ID built in. And then we're going to say, for the post within index,
we're
going to just replace it with our new post underscore dict. So that's how we update that
specific spot
in the array. And so let's give this a shot. So if I do get all posts, right, we can see we
have
post one, and then the title is title of post one. And then let's go to our update. And we can
see that we're going to change the title to be updated title. And let's see if that works. Okay, so
it
looks like it worked. We got back the new post. So let's do a get all posts again. And let's
see
if it actually did update. And we can see that it did in fact, update. Now, just to just to
make
sure everything else is good, let's also change the content. So let's say this is the new
content.
Let's send an update. And then we're going to run this again. And we can see that the content
is
updated. So now we're able to successfully update each and every post. And that's going to
wrap up
all of our CRUD operations. So you can see that building out an API, building out your CRUD
functionality is fairly straightforward within fast API. We've got one more lesson where
we're
just going to restructure our code a little bit just to make things a little bit cleaner. And
then we're going to start getting to the fun stuff, which is working with databases. So I'm going
to
show you guys how we can actually get a Postgres database installed on our machines and
running. And then we'll actually start saving data into an actual database. In this lesson, we're going
to
talk about one of my favorite features of fast API. And that is the built in support for
documentation.
So normally, when you build an API, you're going to want to create documentation so that users
know how to use it. And one of the challenges with that is that it's not exactly a simple process.
And
anytime you make changes to your API, you have to make sure that you update your
documentation. And
it's very easy to forget to do that with fast API, fast API automatically generates the
documentation
based off of the path operations that you've defined. So it does it all for you. And you don't
have to write a single piece of code to actually get the documentation in place. And so to
actually
see the documentation, you just go to the normal URL, but you just do slash docs. And so this
is
going to show you the built in documentation powered by swagger UI. And so what's really nice
is and if you've ever looked at the documentation for our some other API's, you may have
noticed
something that looks similar to this. And that usually means that they used swagger UI as
well. So fast API has built in swagger UI support, and it automatically generates all of the
routes,
and the documentation for all of your paths. And so if you take a look at all of our
operations, our path operations, we've got the one for getting all posts, we've got the one for creating
posts,
we've got the one for getting an individual post, we got the one for updating an individual
post, as well as deleting a post. And what's really cool about this is if we do hit the drop
down,
right, it's going to show you how to use this specific path, or how to use this specific
API
endpoint. So if you want to actually try it out, you can do that. So you could say try it out.
And then you can just hit execute. And so now it actually ran the query against our API
server,
and it shows us what the output is. So these are the two documents that we have, or the two
posts that we've created. And we can do this with all of the API endpoints. So if we
want to create a new post, it's going to give you an example of the data that we can pass
in,
so we can try try it out. And then it's going to give us all the fields that we have to pass
in, and then we can just customize this. So if I want to say, creating new post with
documents,
and then I'll just for content, I'll say this is really cool.
We'll give it a random rating, we hit execute, right, it actually runs that it's going to
show
you how to perform the same actions using curl as well. And then it's going to show you the
data
that we got back from our API server. And we can see that we got a 201 response code. And so I
want
you guys to just kind of play around with this, get familiar with it. So there's going to be
times where you may not even have to use a tool like postman, you could just test things out
using
swagger UI in the built in documentation. And what's even nicer about this is that we
have
built in support for two different types of documentation. So this is using swagger UI, but if
you use redox, sorry, there shouldn't be an S at the end, it's just redoc, right,
you could see a different format of the documentation using a tool called redoc. So it's going
to fundamentally do the same thing. It's just a different documentation tool. So it's
going to look a little different. Feel free to use whichever one you ultimately prefer. But by
having it automatically get generated by fast API, you don't ever have to worry about
updating
anything. So if you take a look at like create post, right, we've defined the pieces of
data
that we need. And in the future, if we decide that the user needs to pass in another
field,
maybe like the date that they wanted to get published, it'll automatically update this, you
don't have to tell it you don't have to manually go in and update it yourself.
All right, guys, so we're going to make one slight change to how we structure our code. And
what I want to do is instead of just having my main file within my base project
directory,
I want to actually create a folder called app. So that's going to store all of our application
specific code, and then put that main file in there. So it's a very small change. But we
do
have to change a couple of things to make sure that it doesn't actually break anything. So I'm
going to create a new folder. And I'm going to call this app. And so this in Python, if
you
aren't too familiar with how Python works, Python has a concept of packages, and a package is
nothing
more than a fancy name for a folder. However, for something to properly act as a
package,
Python requires you to create a dummy file. And this file is called underscore underscore in
it,
underscore underscore p y. All right, and so that's going to turn this into a proper Python
package.
Just know that you have to add this file, you don't have to put anything in the file. But any
time you create a new folder, just make sure you create a file with this exact name. And
that's
going to ensure that it is in fact a Python package. So now that this is an actual
Python
package, we can just drag this main file into our app folder. Right, and then now we're going
to get
an error. And that's perfectly okay. So we're going to cancel out of this. And I want you
to
take a look at the command that we run to start our application. So the command to start our
application was we do uv corn, we do the name of the main file, which is just main. And then
within
main, if you take a look at what's the name of our fast API instance, it's called app.
However,
now when we run this command line, this command, it's going to look for a file named main.
However,
there's no file named main within our base project directory, because it's now been moved into
app. So to actually reference the main file that's inside of a Python package, it's pretty
simple,
you just put the name of the package beforehand. So you do app, and then dot. So it's going
to
tell uv coin to look inside the app directory for a file name main. So then we go in
there,
we see the main file. And then within our main file, we look for the app instance, which is
our fast API instance right here. Let's give this a shot. Let's see if we broke
anything.
It looks like everything started out fine. I'm just going to do a couple of requests. So this
is going to be just get an individual post. Looks like everything is good. I just want to make
sure
that there's no errors. And yep, everything looks good. So I think this is just a little bit
better of a way to structure our application, we're going to put all of our app code moving
forward
inside of this app directory, so that we can keep everything else separate. I think at this
point, we have a basic understanding of how to work with fast API,
and how to set up basic routes and work with path operations. Until now, we've been storing
all of our posts in memory. And I think we've made it to a point where we are ready to now start
working
with databases. And so if you don't know what a database is, a database is a collection of
organized data that can be easily accessed and managed. And so when it comes to any kind
of
application related data, so things like users that have registered posts that have been
created, all of these pieces of information are going to be stored within a database. So it's going to
be
stored on disk, so that we can retrieve this information at a later point in time. Now, when
it comes to databases, we never actually interact with the databases directly.
Instead,
what we have is a database management system that's going to sit in the middle. So when
we
want to perform an operation on a database, we're going to send that request to a database
management system, that management system is then going to actually perform that
operation,
and then it's going to send the result back to us. So we never talked to the databases
directly. Instead, we have a piece of software that sits in the middle and access the brains behind
the
database. Now, there's two major branches of databases, there's relational and no SQL
databases,
relational databases are usually SQL based databases. And we're going to cover that in the
next slide or so. But these are just some of the more popular relational and no SQL
databases
that you guys have probably heard of. In this course, we're going to work exclusively with
relational databases. And more specifically, we're going to work with Postgres. Now, if you
ultimately
want to learn how to work with MySQL or Oracle or SQL Server or any of the other databases,
keep in mind that at the core, they are fundamentally no different than Postgres,
you know, 90, I would say 97% of the things that we cover when it comes to Postgres is going
to be almost identical in all of these other databases, because they all at the end of the
day,
use SQL. However, SQL, there is a little bit of slight nuances when it comes to each
database,
each of them will implement it in a slightly different way. So you'll see that certain SQL
commands aren't available in others. But for the most part, all of the core things that we
cover
from a SQL perspective is going to be applicable to any of the other relational based
databases.
And so let's talk about SQL now. So SQL or structured query language is a language that's used
to communicate with the database management system. And so, you know, when we want to
perform
an operation, we're going to send a specific SQL statement to the database management system,
it's going to then take that statement and then perform the operation on the database.
And then it's going to send that result back to us. So SQL is the language that we use to
communicate with the database management system. Now, in the next video, what we're going to do is we're
going
to install Postgres on both Windows and Mac, depending on what you're using. And there's an
important thing to note when it comes to installing Postgres, or really any database, when you
install
an instance of Postgres, what we can actually do is we can carve out multiple separate
databases,
right? And this is kind of confusing at first, because you're thinking, well, we installed
Postgres, we have a database. Yes, but what we can do is we can actually create two
different
databases that are completely isolate, isolated, and have nothing to do with one another. And
that way, if I have application one, we can create a database just for app one. And then if I have
a
second application that I create, this application can have a separate database as well. And
there, these two databases are going to be completely separate, and completely isolated so that
they
don't step on each other's toes. So it's pretty cool. And it's a pretty awesome feature to be
able to kind of carve out your Postgres instance to multiple databases. And keep in mind that
you
don't necessarily always create a separate database in database instance for each
application,
right? There are scenarios where you might actually create an app that's going to make use of
more than one database instance within Postgres. However, those are very specific
situations,
maybe if you want some sort of multi tendency or things like that. However, we're just going
to create one individual database instance for our application, that's going to handle
everything
that we need. Now, when we install Postgres, what's going to happen is that the
installation
process is going to create a database within the Postgres instance, called Postgres,
little
confusing, right? It's Postgres within Postgres. But it's actually going to create this
database named Postgres. And the reason we need to do this is that if you ever want to connect to a
Postgres
instance, what you have to do is you have to specify a database that you want to connect to
some some kind of default database that you want to connect to, you can't just connect to
Postgres,
you have to specify a database. And so that's why the installation process creates one
database for
you so that you can have something to connect to. However, we won't necessarily need that
instance
once we create our own. However, I usually just keep it in by default, because it's not going
to hurt anyone. In this video, we'll take a look at how we can install Postgres on a Windows
machine.
So let's go ahead and just search for Postgres. And it's almost always going to be the
first
instance. So just go to Postgres ql.org. And then we can just select the downloads
button.
Here, go ahead and select Windows. And then up here, just like the download the installer
link.
And so here, you'll see all of the downloads options. So right now, the latest version is
13.4. So I'm going to just download that one, you may see a newer version, feel free to
download
that nothing's going to really change between each version. Once that's downloaded, start
the
installer. If you see this pop up, just select Yes. With the installer open, select
Next,
you can leave the default installation directory, no one usually messes with that. Now here,
you can select the different components that you want to install with your Postgres
instance. So for the first thing that you have is the Postgres SQL Server, that's obviously
what you want to download. The next one is PG admin. So this is a GUI that can be used to manage
your
Postgres instance, we're going to be using that to actually manage Postgres. So you want to
make sure that you've got that downloaded stack builder, this is a an extra feature that can be used
to
install extensions and extra features within Postgres, we're not going to be installing any of
those extensions. So there's no need to have stack builder. However, it's not going to
break
anything if you do leave it checked. And then we'll we won't be using any command line tools
to manage Postgres. However, go ahead and just leave that check just in case, you know, down the
road,
you actually do want to use it. All right, and then once again, leave the default data
directory,
then you have to pass in the password for the super user. So make sure you remember this
password.
Then you got to specify the port that you want your database to be listening on. The default
Postgres port is going to be 5432. You can change it if you want, but you have to make sure that
you
update everything else. When it comes to creating our code to update that port number, I'm
going to
leave it as default as do most people. And then just hit Next on this window, and then next
here,
next, and then it's going to start the installation process. And once that's complete, just
hit Finish. And then like I said, we're going to be using the
PG admin GUI software to manage our Postgres instance. So if you just search for PG
admin,
you'll see that it's right there, just like that and open it up. And then at that point in the
next video, we'll start taking a look at how we can use that to not only create individual
database
instances, but also create tables and things like that. In this video, we'll take a look at
how we
can install Postgres on a Mac machine. So let's search for Postgres in Google. And the
first
result should be the one that we're looking for. And within the Postgres website, just like
the downloads button, and this is going to show you the different platforms it supports. So
we're
going to install this for Mac. And then select Download the installer. And then here, we
could
see all of the versions that we can download, we're going to download whatever the latest
version is. So in my case, it's 13.4. If you're watching this video in the future, it's probably going
to
be some other version, but it should all be relatively the same. So we'll just download under
Mac. Alright, and once it's finished downloading, just go ahead and select the
dmg file that you downloaded. And then we'll set up the installer. And if you get this pop up,
just like to open and then specify your password. With the installer window open, select
Next,
leave the default installation directory. Now within here, by default, it's going to
install
four different components. I'm going to walk you through what each of these components does.
The first one is the Postgres server. So this is the actual Postgres database. So we definitely
want
to install that below that is PG admin. This is a GUI that can be used to manage your
Postgres
databases. We're definitely going to install that because that's what we're going to use in
this course to actually manage our database. Stack Builder is an extra, almost like an
extra
installer that's used to install extra extensions that give Postgres some extra
functionality,
we're not going to use any of those extensions. So go ahead and uncheck Stack Builder. And the
last one is command line tools. So we can manage Postgres through the GUI, as well as the command
line.
In this course, we're going to stick exclusively to the GUI. However, I would still recommend
downloading the command line tools, just in case you ever do want to use
them. And then specify the default data directory, that's fine. Then you want to give the
password
to your Postgres instance. So this is kind of like the root user equivalent. Here, we specify
what
port we want our Postgres instance to run. This is the default Postgres port. If you want to
change, you can do it. But just make sure that you know, whenever we get to actually coding out our
API
and specifying what port we connect to the Postgres database, make sure you update those
accordingly. However, I recommend just keeping it as the default. And then everything else
just
keep hitting next. And then it'll start the installation. All right, and once that's
complete,
just like finish, it's going to be at that point, Postgres has been successfully installed,
then what you want to do is just hit the little search icon. I want you to search for PG
admin.
All right, and so if you want to open up PGV admin, which we'll cover in the either the next
lecture
or the lecture after that, just search for it, select it. And that's going to open up the GUI
tool used to manage your database. Before we get started with configuring our Postgres
database,
there are a few things that we need to cover. And one of those things is the concept of
tables. And this is a very important concept when it comes to working with relational databases. So a
table
represents a subject or event in an application. And so what exactly does that mean? Let's use
a
example. Let's say we're building out a e commerce application, you're going to have a table
that
represents each part of your application. So you're going to have a table for all of the users
that have registered, you're going to have a table for all of the different products that you will
plan
to sell on your website. And then you can have a table for things like purchases and reviews.
So each table represents a different subject or event in an application. And what's amazing about
these
tables is that all of these tables are going to form some sort of relationship. That's why
it's referred to as a relational database. And if you think about it, right, the tables that we
have
listed right here, they're all going to have a very basic and obvious relationship, right
purchases,
when a user purchases something that automatically implies a relationship, because every
purchase order has to be associated with the user account that actually filed that purchase order. And
a
purchase order is going to have a list of products that the user wants to buy. So we could see
right
there, all of the tables that we have already form some sort of relationship. And it's
really
important that when you design your database, that you figure out what are these relationships
beforehand so that you can design a very efficient database, right, just like if we're building
a
social media application, right, we are you, we know, we're going to have a table for users,
we're going to have a table for posts as well. And we already know that there's going to be
a
relationship between the two because every post has to be associated with a user because users
create posts. So you can see that, you know, all data has some sort of relationship with one
another.
Now, when it comes to tables, we have columns, and we have rows, a column represents a
different
attribute. And so if we're building out a table for users, we would create a column for for
the
name. So this column represents the name of the user, we might create a column for their age,
their gender, you might create another column for the email that they used to sign up, you
might
have a column for their billing or shipping address, right, it's up to you to figure
out
what are the different attributes we need to model a specific user, then we have rows, rows
are very
simple, right, each row represents a different entry in the table. So we have a users table,
then each row represents a unique user. So row one, this is the user, Vanessa, row two is
going
to be the user Carl. So each row just represents a different entry within the table. Now
databases
have data types, just like any programming language. And the reason why this is important is
that when you create a column within a table, you need to specify what kind of data type
you
want to use. And that decision is going to be ultimately based off of what attribute this
column
represents. And so we have data types, just like any other programming language like
Python.
And so if we were building out a table for, you know, social media posts, one of the things
that
we might have is a column for what are the total number of likes or retweets. And so for
something
like that, you would want some sort of data type that numerics, you know, a data type that
models numbers. And in Python, you know, we have two main data types, we have integers, and we have
floats,
integers being whole numbers, floats, giving you decimals, or Postgres has the same thing,
right, it's going to have integers, it's going to have decimals, and a few others, keep in mind
that
you don't need to worry too much about that, just understand that there are numeric data types
within Postgres that are just like Python. So when it comes to the number of likes of
postgifts,
you know, maybe we would use something like an integer, because we're not going to have a half
a like or anything like that. Then if you're trying to model, you know, someone's name
or
email, or address, right, we would use some form of text. And within Python or any
programming
language, you have strings that allow you to represent text. Within Postgres, we have the same
thing, it's just called something different. We have varchar, which is just short for
varying
character, which is once again, just a string fundamentally. And then we have text, right,
they're both going to do the same thing, don't worry too much about the differences, just
understand
that Postgres does allow you to work with text, just like any other programming language, it's
just called something different. And then, for Booleans, Postgres, and Python,
they both support Booleans, they're going to work the same way, it's either true or false,
very simple. And then within Python, we have lists. So anytime you want to model, you
know,
multiple instances, or a list of something, we have arrays within Postgres. And you'll see
that when you work with relational databases, you'll see that arrays aren't used very, very
much,
because a lot of times if you're using an array, it may be better to take that column and just
convert it into its own table. But like, like I said, you know, don't worry too much about
that,
we'll go over that in some upcoming upcoming lectures. When we create a table, we have
to
specify something called a primary key. And a primary key is a column or a group of
columns
that uniquely identifies each row in a table. But what exactly does that mean? Well, we need
to
tell Postgres essentially, how can we uniquely identify each entry in our table, each row?
And
so we have to give it a special column that ensures that every entry for that specific column
is
unique. And keep in mind that we can only have one primary key per table, you can't have more
than one. However, a primary key can span multiple columns. However, let's keep things simple
for
now. And just assume that primary keys are really just one column. And so a lot of times when
you
create a table, what we have is a column for a unique identifier. So anytime you create a
new
user, usually you're going to have an ID associated with that account, and that ID is going to
be unique. And so the ID associated with john is going to be 77498. And that's how we
uniquely
identify each user based off of that ID. And so it makes sense to select that as the primary
key,
because we already know that each ID is going to be unique. So it fits the perfect definition
of what a primary key is. Right. And so the only requirement for something to be a primary is
each
entry must be unique, no duplicates, we can't have the same ID for another user. And so
you're
probably thinking at this point that hey, we're going to create an ID column for every table
and then make that the primary key. I'm going to tell you that that's not quite
correct.
The primary key doesn't have to be the ID column always. In fact, your table doesn't even have
to
have an ID column, right? It's up to you to decide if you actually want to have that there's
certain instances where you may not want an ID column. And so if you don't want to use the ID
column,
or if you don't have an ID column, you need to ensure that you have some column that's able to
uniquely identify each and every entry. And so one good example of that when sticking
to
the users table example, is that generally in an application, a user can only sign up with an
email
once. So we know that an email is always going to be associated uniquely with one account,
right,
we can't have two users with the same exact email, that's not going to work, that's usually
not allowed. So we could definitely use our email column as a primary key. But there's plenty
of
other examples, right, phone numbers, right, generally, I can't sign up with the same phone
number for two different accounts. So we know phone numbers are going to be unique. For
the
guys that are in the United States, you know what a social security number is, two people
can't have the same exact social security number. And so there's a lot of different things that you
can
use to choose for the primary key. Just keep in mind, it's not always the ID. So that's the
main
thing I wanted to focus on in this slide. Now, as we go through creating our table, and
creating
each of the individual columns that represent a certain attribute for that table, we can add
in
extra constraints for each column. So we can put in extra little conditions. And so we
already
talked about the column that's going to be the primary key. And we know that the primary key
has to be unique for each and every row. But what happens if we have another column that isn't
the
primary key, but we want to ensure that each and every entry has a unique value for that
specific
column, right? So take a look at the name column. In this case, let's say that we won't allow
two
users to share the same exact name. Is there any way we can tell Postgres to perform that
check for us? Well, we can add in what's called a unique constraint. So we can when we create the
table,
say that I want to make this column unique by adding the unique constraint. And so that way,
anytime I add in a new user, it's going to check the name, and it's going to check to make
sure
that there's no other users with the same exact name. And if there is, it's going to throw an
error. And we can do this with any column, you can do with all your columns, if you wanted
to,
that's not necessarily something you're going to do. But I just want to make sure you guys
understand that, you know, we can perform extra checks for each entry. By adding in a
constraint,
the first constraint that we covered is the unique constraint, but there are plenty more. Now,
the next constraint is called the null constraint. And when it comes to creating
columns, by default, Postgres allows you to leave a column blank, essentially. So I can add in
a new
user. And I can say that I can leave the name column blank. So instead of having Vanessa
for
the first one, I could just leave it blank and not put any data in. And so what Postgres will
do is it'll put in a value of null because we didn't provide it. And I can do the same thing for
age,
I don't have to pass in an age by default. And I don't have to pass an agenda by default as
well. And it will put in a null value for each and every one of those. But let's say, we want to
tell
Postgres to perform a check and say, hey, I don't want you to be able to create a user that
doesn't
have an age. So what we can do is we can put in a constraint. And this constraint is called a
not null. That means we are not going to allow it to be null, right, we are not going to allow
Postgres
to use a null value. So if we try to add in a new user called Carl, and we don't give it an
age,
well, Postgres sees that this column is not allowed to be null, and it's going to throw an
error because it's not null. So that's the second constraint. And I think these are the two
main
things that I want to focus on. And I think at this point, we have a good enough understanding
of the different things that we can do with tables, columns and rows, that we can go ahead and
start
playing around with Postgres, and really digging deep into how tables are created and how they
work.
In this video, we'll start taking a look at PG admin, which is the GUI that we use to manage
our Postgres instance, we'll take a look at how we can define our tables for Postgres, we'll take
a
look at how we can create entries in our database, as well as how to modify and delete them as
well.
So go ahead and search into under your applications for PG admin. And you'll see that once it
pops up for the first time, it's going to ask you to set the master
password for PG admin. And it's important to understand that this password is for the
PG
admin GUI, it has nothing to do with the password that you created during the Postgres
installation,
this is strictly for PG admin itself. And the reason to ask for a password is that
ultimately,
we can store our passwords for connecting to our different Postgres instances within PG admin
so
that we don't have to enter it every time we want to connect to our database. And so that's
why we have to create a master password so that we could store all of the individual passwords for all
of
our Postgres instances. So I'm just going to create a random password in this case. And keep
in mind,
it does not have to match the password that was created for your Postgres instance, these two
have nothing to do with one another. Now, the first thing that we want to do is specify the
connection
details for connecting to our Postgres instance. And what's nice about PG admin is that we
can, it'll remember all of the servers that we've ever connected to so that we don't have to
manually
input this information every time we log in. Now, PG admin has done a little bit of the work
for us
automatically. So if you see under the server section, this is where we define our individual
Postgres instances, just hit the little drop down arrow. And you'll see that there was
a
server called Postgres ql 13 already created for us. So by default, when you install
Postgres,
we know that we're going to have a local Postgres instance running on our local machine. So
it's going to automatically set up the connection details. For that instance, the only thing you
have
to provide is the password for connecting to that instance. And so now we want to pass in the
password for the Postgres user for the Postgres instance. And this password isn't the password that we
just
created a few seconds ago, this password is the password for Postgres itself. So this is the
one
we created during the installation process. So go ahead and put that in there. And then you
can
choose to save this password so that you don't have to reenter it. I'm just going to skip that
for now.
And so now it's taken that password, and it's now connected to our local Postgres instance.
And so you'll see a little bit of information about things like, you know, what's the
sessions,
what are the transactions per second, so you'll see a lot of nice little pretty charts.
However, I want to make sure that you guys actually understand how to define a new server
instance.
So what we're going to do is we're actually going to create this from scratch. So I'm just
going to select this, we're going to select remove server. I'm going to do this straight from scratch. So
I'll
do create server. Let's give this a name, I'm going to call this my local Postgres
instance,
because that's what it is local Postgres. Then we have to specify the connection details. So
here we give the IP address that the Postgres instance runs on. Because this is running on our
local
machine, we can just say localhost. However, if this was running within AWS or some cloud
provider,
that cloud provider would have would provide you a password, sorry, an IP address, or even a
domain name to connect to. And that's what you would specify here in this case. But since
we're
connecting to our local machine, we just say localhost. Then here we give the Postgres
password.
Right when we did the installation for Postgres, we use the default port. So that's why we
could just leave this as 5432. And then as I mentioned, when we connect to Postgres, we always have
to
give it a the name of a database to connect to, we always have to provide the name of a
database to connect to. Since this is a fresh installation, the only database that's going to be there
is
going to be a database named Postgres. So it's going to connect to that by default. Then we
have to give the username and password. And we haven't created a user yet. So there's
always one default user, this is a super user. And the username for that user is always
Postgres,
a little confusing, because the database name is Postgres, the name of the actual DBMS is
Postgres, and then our username is also Postgres. So try not to get all three of those things confused.
Then
here we could save in our password. So this is where we put in our Postgres
password.
And then if you want to save this for a future so that you don't have to enter in every time,
you can go ahead and do that. At that point, hit Save. And then you can see that once
again,
we created this from scratch. Now within PG admin, there's going to be a lot of menus, a lot
of things that you see. And you may be a little overwhelmed. But I want you to keep in
mind that a majority of the stuff we won't really even care about. Alright, so when you focus
on just things that we care about, you'll see that there aren't many things involved when it
comes
to PG admin. And so like I said, within a Postgres installation, we're going to have
individual
databases, right? So we can create a separate database for each of our applications. So under
our server, right, you'll see a section called databases. So this looks a little
interesting,
let's click this. And then if we drop this down, or expand this, you'll see that we have our
one
default Postgres instance that was created for us during the installation process. Now we're
not
going to touch this, we're just going to leave this there. But let's create our own Postgres
instance. So right click on databases, it create, and then database. And then here,
we just want to give the database a name, I'm going to call this, I usually just name it after
my application name. So since this project is called fast API, I'm just going to call this
my
fast API database. And you'll see that there's a couple of other windows, we won't touch any
of
these. However, go to the last section right here, where it says SQL. And as I mentioned
before,
SQL is the language that we use to actually talk to Postgres. And, you know, since this GUI is
here
to manage Postgres, it's going to make our life a little bit easier. But at the end of the
day, it's just passing in SQL statements. And so because we created a database through the
GUI,
it's kind of abstracting a lot of the SQL away from us. However, what's nice about PG admin is
it's going to give you the underlying SQL that's used to actually create this database. So we
could
see that it's just a command that says create database, and then the name of the database we
gave it. Here, it's just specifying the owner, the encoding and the connection limit.
Technically,
we could remove all of these lines and just take the semicolon and just put it right
here.
And then we can just do create database fast API. And that's how you do it through the command
line. I think it's just nice and helpful that it provides this for us. However,
ultimately, we won't need to do that because we do have PG admin. So we can do it all through
the GUI. And, you know, if you guys are worried that we're not going to cover SQL, believe me, we
will,
but I just want to make sure we start off by, you know, taking a look at the GUI, learning it
the easy way, then we'll learn how to do it the harder and proper way. But once we got this set up,
go
ahead and hit Save. And you'll see that we have our new database right now there's an x. But
as
soon as you click on it, it's going to turn yellow, which means that we have now successfully
connected to this database. And then under this database, you're going to see a lot of different
options.
And like I said, we're not going to touch most of these. Instead, the only thing that we want
to focus on is under schemas. So under schemas, and then under public, and I'm going to move this
over
for you guys, you'll see that there's one important section called tables. So this is where we
actually
create and define our tables. So right click this. And actually, if we do it, if we hit the
dropdown,
you can see there's no tables, but that's to be expected because this is a brand new database.
And let's go ahead and create our first table. So we'll do right click, create, and then
table.
And then let's give this our table a name. So let's say we're sticking with the whole
ecommerce example. So we're going to create a table called products. So this table represents the
different
products that we're going to sell. Don't worry about any of these other fields. It says
the
owner's Postgres because we're logged in as the Postgres user. However, you know, if we logged
in as a separate user, that then you'd see the owner get updated accordingly. If we go into the
column
section, this is where we define our column. So this is where we're defining what is referred
to as the schema of the database. So to add a new column, just hit the plus icon. And so
now,
the first thing that we need to do is let's figure out what attributes does a product need.
And, you know, a product is going to need a name. So let's give it a name. And so we'll call
this
column name because it represents the name of a product. All right, then we have to give it
the
data type, as I mentioned, there's going to be different data types that closely resemble
or
match the data types of a programming language. So a name is going to be, you know, a certain
amount of text, right? So we need whatever data tape represents text. And so we've got a
couple,
right, we do have text. And then we also have a varying character or character varying, this
is the one I'm going to ultimately use. So we're going to select character varying.
If you actually want to see the difference between these, I recommend you go online, search
search for Postgres data types. Right. And then just it's usually chapter eight
of the documentation, you can select this. And then this is going to give you a breakdown of
the different data types. And it's going to break it down based off of, you know, numeric
types,
we've got a date, time, a Booleans enumeration enumerated, we've got, you know, if you want
to
represent IP addresses, we've got a data type for that. So there's a ton of different data
types, definitely take a quick read over all of these. However, we're just going to stick to the
basics.
So if you don't want to go through all of that, then we'll stick to just the core fundamentals
for now. Anytime we're working with text, we're going to use character varying. And then as
I
mentioned, we've got a couple of options. So we've got the not null. And so right now it's set
to
no, which means that technically, we can leave this entry blank. So we can create a brand
new
product with no name at all. And then Postgres is going to put in a value of null if we do
that.
But if we think about our application, right, would we really want to be able to create a
product
without a name doesn't really make sense. So I think we should set this to be yes, so that it
actually throws an error if we try to do that. And you'll see a flag for a primary key. So
we
get to specify which column is ultimately our primary key, I don't think the name of a product
should be a good primary key, we need the primary key to be a column that really emphasizes
what
makes each entry unique. And I don't think the name actually does that. So we're going to
leave that set to no for now. And we're going to add a new new column. And this time, we'll add
in,
let's say, the price. And so now we have to specify a data type. So for numeric data
types,
we have an integer, which is going to be whole numbers, which probably isn't a good data type
for prices, because we need decimals. We do have numeric. And there's a couple of other ones.
So
if we go back to our documentation, hit numeric types, right, we've got numeric. So this
allows
us to specify also decimals as well versus integers. And then we also have floating
point
types as well. Either one of these can be used the they operate a little bit differently. But
both of them will work. But since we since I want to keep things simple, and I don't want to
confuse
you guys too much, I'm just going to use an integer to define a price. And I know technically
there's no application that would ever use an integer to represent price. But you
know,
I want to keep this simple. And I actually want to show you guys something unique. So you see
that there's a couple of different types of integers, we have, first of all, a
plain old integer, we've got a small int. And then we've got a big end. So what is the
difference
between all of those? Well, it's the number of bits that we have. So a small int has the
fewest number of bits, which means that the maximum number we can go to is going to be a lot
smaller,
versus a traditional integer. And if we need to go up to a really high value, we're going to
use
big int, which is going to have the most number of bits, right. And if you want a good
explanation, just search for what is the difference between big int versus int. Right. And so we can see
here,
the difference between them is you can see the minimum value, and you can see the maximum
value. And so I think that's what minus 2 billion to 2 billion. So we get it in the negative and
the
positive direction versus a big in which nine, negative nine to 9 billion. So you got to
figure out what's the best fit for you if you don't, if you don't think you'll even hit 2
billion,
then there's no reason to go to a big int. So I'm just gonna leave this as a plain
integer,
we could definitely get away with a small integer. But let's not worry about that. I don't
think we're ever gonna have a product that's going to be $2 billion, but that's fine. All right. And
then
once again, we have the not null option. So it doesn't make sense to create a product
that
doesn't have a price. So let's make sure that's not nullable. And then the length and the
precision,
this is for floating points. So you can say like, how many decimals do you want to support it
for?
For an integer, though, it shouldn't matter. And it looks like I didn't properly select this.
We'll do integer. All right. And then the last thing that we need is each product should
have
a unique ID, right? And like I said, a lot of our tables are going to have an ID column. So
here,
we're going to create a column called ID. And so what kind of data type should we use for
this?
Well, I think what makes sense is an integer, right? An ID is just going to be some random
number.
So we could use int, big int, or small int. And so let's just say we'll use an
integer.
I'm going to search for integer. However, I don't actually want to leave it like
this.
I did mention in one of the previous lectures that most databases support the ability
to
automatically generate a ID for you. So I don't want to have to be a be the one responsible
for
creating an ID and then putting that information into Postgres. I want to just provide the
name and the price and I want Postgres to generate a unique and random ID for that product. So how
do
we do that? Well, it's pretty easy. There's a there's a data type called serial. And so a
serial
is just a regular integer. However, it automatically creates a random number. And it's
actually not a
random number. What it does is the first product we create will get a value of one. The second
one
will get a value of two, and it'll just increment by one for each product we add. So it's
going to ensure that every single product we create gets a unique integer value for the ID column. And
like
I said, you know, we do have, you know, small ints, big ints, and regular integers, this is
the same thing, but it's just auto incrementing for those three kinds. So I'm just going to say it's
going
to be a regular integer that increments by one automatically. And then this is going to be
what's
going to be the primary key, right? We know that the ID has to be unique, I think that makes
sense to make it the primary key. And I think for now, this is a good enough starting point to learn
about
SQL and Postgres in general. So let's save this. And we have then successfully created our
first
table. And so under tables, you'll now see our products table. So let's start playing with
this
product table, let's create entries, let's delete entries, let's really start to have fun with
it. So right click on it. And select view, edit data. And then you really select any one of
these,
if you select all rows, what it's going to do is, it's going to fetch every single entry that
we have in the database and show it to you. We don't have anything in the database. So doesn't
really
matter. If you select first 100 rows, it's going to grab the first 100 rows in the database.
And
if you select last 100 rows, it's going to grab the last 100. Now, if this was a production
server or something like that, we might have millions of entries. So you may not want to do all rows
and
then just get flooded with all of this data. So you may want to select one of these or filter
out the rows based off of some criteria. But we have no actual data on it. So I'm just gonna select
all
rows. And you'll see that within PG admin, we have these different tabs. So it opened up a new
tab.
And you see that there's like a little pop up window. So if you can pause it and rewind, it
says that it actually ran a query. And so what it did is it ran a query to retrieve all that
all
the rows because that's what I selected, right? I say like, I selected, you know, get me all
of the all of the rows by selecting all rows. And if you look at this query editor right
here,
this is the actual underlying SQL that it ran. So it ran select star from my products table
order
by ID in an ascending fashion. So it's going to grab the first product with the lowest ID
number,
and the next product with the second lowest ID number, and it's going to keep incrementing up,
we've got nothing in our database. So it didn't return anything. But I think it's cool that
it
does show us the underlying SQL. Now, like I said, we don't have anything in our database at
the moment. So let's go ahead and create our first entry. And so I'm going to create
select
this column, and I'm just going to give it a name. So this is going to be a let's say a
TV.
And let's give it a price, I'll say this is $200. And then the ID, this is going to
be
automatically generated. So we don't need to provide a value. And so at this point, you may be
thinking, well, it looks like we created an entry, right? We have our TV price of 200.
Well, not exactly. To actually store this data in the database, right now, we're just
proposing
changes, we have to select this button right here. So if you hover over this, this says save
data changes. So when you select this, this is actually going to push this data into the
database.
So let's do that. You can see that the data was saved successfully. And now it's actually in
here,
in the database. So let's create a new entry. I'll say this is a DVD player. I don't
know
if anyone uses these anymore. And then let's give this a price, I'll say this is
$80.
And then we can we can just keep adding as many as we want. So let's add in a
remote
is going to be 10 bucks. And you'll see that anything that's kind of bolded, so you
notice
how TV isn't as bold as DVD and remote. So bolded means we haven't actually pushed it out to
the database, it's just proposed changes. So we can push out both of these at the same time by
pressing
this again. So we push that and then you can see that they got pushed out and take a look at
the ID column, right? The first item got an ID of one, the second one got an ID of two, and then
three,
and it just keeps it's just going to keep incrementing. Now, let's have a little fun. Let's
create a new item. This time, let's create let's make it a microphone or something.
And let's leave this price column blank. And let's see what happens. What? So now if I press
this,
take a look at this error, null value in column price. So we left this value empty. So
Postgres
interpreted as null, but we set the column to be not null. So we told posters to do a check
and say, Hey, listen, if we ever don't provide a value, we want you to throw an error. So this is forcing
us
to provide a value for this because creating a product without a price just doesn't seem
to
make sense. So let's give this a price. Save that. And so now that worked. And then this
time,
let's try creating a product with a price. But no name this time, right? So what do you expect
to
happen? Well, since we set the column to be not null, it's going to throw an error or it
should
hopefully. And look at that, null value and column name. So once again, it's not going to
be
supported. And it's gonna throw an error. So let's just give it name. Let's say this is a car,
the world's cheapest car, hit, okay, save that. That's been successfully added. Alright, so
now
that we've kind of played around with it a little bit, let's make this a little bit more
interesting. And let's add in a brand new column to our products table. So right click on the products
table
right here and select properties. Then under columns, we're going to add in a brand new
column. So we're just going to follow the same steps. And this column is going to represent if
a
an item is on sale. So I'm gonna call this is underscore bull. Sorry, not is underscore bull,
it's going to be is underscore sale. And it's going to be a simple Boolean as a data
type,
because they are going to be set to true, which means the items on sale, or false that the
item is not on sale. So we'll do Boolean. And then everything else, we're just going to
leave
the by default. So for not null, we're going to leave this to be no, so we are going to
allow
the user to input a value of null in this case. However, what we're going to do is we're
going
to add in an extra constraint, I guess, actually, we're not going to we're going to set a
default
value. So if the user does not provide a value for is underscore sale, we're going to give it
a
default value and say that by default, if a product doesn't receive a property for a sale,
it's going to be set to false. So it's not going to be on sale. So let's hit save. All right.
And
then right now, you don't see a new column. But that's because you don't see the new is on
sale column. But that's just because we need to refresh this page. So let's go under object, I think
we
could do it here. I think it should be a refresh. I didn't do anything. So I think we're just
going
to do the same thing as we did before. There should be a way to refresh this. But it looks
like if I just press it here, it doesn't do anything. So instead, I'm just going to do a view, edit
data,
all rows again, it's gonna open up a brand new tab. And now we can see the is underscore
sale
column here. And what's awesome about this is that because I provided a default value, it
went
to all of the other items in my database that didn't that were created before this column was
created. And it provided the value of false because I said, Listen, if, if the value is
null,
or if we didn't provide that data, go ahead and put in the default value of false. And so now
if I create a brand new item, and I'll say this is for a pencil, and the pencil is probably only
$2.
Now, since this can be set to null, I'm going to just leave that blank. And now if I save
this,
take a look at this, it gets the default value that we provided. So you'll see that for a lot
of your columns, you're going to want to provide default values, if you expect it to be
frequently
inputted without a value, and you want it to receive a default value. And now once again, I'm
going to actually create a brand new column once more. And then we'll go to
properties,
we'll go to columns. And then we'll add a column. And we'll call this column inventory. So
this is
how many of the items that we have in stock. And so here, I'm going to this is going to be an
integer. And I think it makes sense for in our application to have a, a value for our
inventory,
we shouldn't leave it now. So I'm gonna set it to yes. And I want you guys to see what
happens
when we hit save. Because the big issue is, right, this column doesn't exist at the moment.
And so
all of these items were created before there was an inventory column that was created.
However, this column has a not null value, but it doesn't have a default value, right, we gave his sale
a
default value. So Postgres was smart enough to go into all of these pre existing items, and
give it a value of false. Well, what's going to happen with the inventory, I didn't give it a
default
value. So let's see what happens. If I hit save, eight, there was an error, it says column
inventory of relation products contains null value. So it's basically saying all of these products that
you
had before, don't have a value for inventory. So at this point, you know, you have a couple
of
different options, you could delete your all of the previous products if you wanted to. And
since
this is a development environment, that's not a big deal. If you're in a production
environment, that could lead to issues, obviously, you don't want to delete your production data. So we
could
just give it a default value like we did before. And so I can just go back into const Whoops,
not there, we want to select this little button, go into constraints, and we'll say the default
value
is mostly there's gonna be zero items. So let's hit save. And once again, I'm just going to
open
this up, I still don't know how to refresh it. I know there has to be a way. I can't find it.
So
it's okay, we could just open up this again. And then we can just delete these, I don't want
too many of these. And I don't know why it's got squished up like that. But because we provided
a
default value, it all it got all values of zero. And so now if I create a brand new item, and
I'll
call this one, this will be a pencil sharpener. We'll give a price of $4 is sale, also to be
true.
That's true. And then if we don't provide an inventory, it's gonna get that default value
of
zero. Alright, so I think we've almost wrapped up our schema for our products table.
However,
I want to add one more thing. And this is something that's pretty common. Anytime you create
an entry
within almost any other table, you usually want some sort of timestamp, right? So what when
did we add this product? When did this user create as account? When did this user create this
post,
right? So anything that we add into our database or an application, it is generally best
practice to store when that item was created, because you never know when you need to know that
information.
So let's create another column. And let's see how we can work with timestamps. So I'm going to
right click on products as usual, go to properties. And then under columns, we're going to add in
a
new column. And we'll call this column created that. And then here, we have to select
something
that represents time. So what I recommend you guys do is just go to the documentation. And
then look
at date time types. So there's a couple of different ones, but we're going to do is
timestamp
with timezone. So we got timestamp without timezone, and then timestamp with timezone, we've
got just the date, and then we've got just the time, but I want the date, time and the
time
zone. So that's the most detailed. So I'm just gonna search for timestamp. And we want
timestamp
with timezone. All right, and we obviously want this field always filled in.
However,
I don't want to have fast API API be responsible for creating the timestamp, I want
Postgres
to automatically create the timestamp for when the entry was created. So I want to be able to
just create a product. And then as soon as it gets added to our database, Postgres will
just
fill it in with whatever that current time is. So how do we do that? Well, we can go into this
right here. So let's configure this add a constraint. And then here, all we have to do
is
for the default value, I want you to just write now. So what this is going to do is the
default
value for any product that gets added, it's going to run this specific command on the CLI. So
this
command just grabs what is the current time, pretty simple, right? Now, I think it's pretty
straightforward, what is how what time is it now, right? And so this will set the default value. So if I hit
save
at this point, right, since we had all of these other items that were created previously
before
there's a timestamp, what it's going to do is it's going to set the create a time to whatever
time it is now, which technically, that's not accurate, because that's not when they were added.
But
these were created before we were smart enough to actually have a timestamp. So if we refresh
this, so I'm just going to do a view edit all rows again.
Right, you can see that they've all been given the same timestamp of whatever that current
time was.
And now if we add in a new item, and I'll add in how about a keyboard, we'll give this a price
of 28 is sale, we can just leave that empty, how many in our inventory,
we'll give it 50. And then we leave this blank. And so let's see if it automatically fills
in
what is the current time and it should be later than this time, obviously. So let's save. And
so if we just double check, yep, we could see that this is about a minute later. And so now
we've
successfully added the created at timestamp. In this lesson, we'll run our first SQL
command.
However, before we do that, what I want you guys to do is create a few more entries within
your database. And so you could see that off camera, I added about, you know, six or seven more
entries.
And you want to make sure that you provide enough varying data to your database. So make
sure,
you know, the price is all different, make sure that the is sale Boolean is, you know, a good
mix of true or false, make sure that the inventory numbers vary enough, and then try
to
create them at different times as well. If you can do that, that way, when we actually go to
make queries, we can see a variety of different output. And that won't happen if you only have three
to
four entries. But once you do that, we should be ready to start our first query. And what we
want
to do is right click on your database. So my database is called past API, and then select
query tool. So that's going to open up a brand new tab. And what we can do is we can run our
query
from here. So here we type out our query. And then we just hit the play button. And that's
going to run our query. So how do we make a query within a SQL database? So first of all, let's start
off
with the most basic query, I'm going to type it out. And then after we type it out and run it,
I'm going to explain to you exactly what each part of the command actually does. But
here,
we'll do select star from products, colon, then once we've typed this out, what we can do is
just
hit the play button. And let's see what happens. So this is going to run the query, we got the
same output here, saying that it successfully ran. And it says 13 rows affected. And so what we can
see
here is it just printed out every single entry within our products table. Now we only have
one
table. So it's essentially just dumping out the entire database. But if we had more tables,
you would see that this would only print out whatever was in our products table. So this command
right
here does one simple thing. And that is, it's going to give us every single row within the
products database. So let's actually break this down. So first of all, we start this command
with
select. So we're basically saying, hey, I want to select these rows. Ignore this for now,
we're going to come back to this. And then what we do is we run from, and then products. And so you
say,
what table you ultimately want to run this command for. So you say from, and then the table
name.
And then it's going to run that query against that table. If we had a table called users, and
we wanted to get every user, we could do select star from users, no different, right.
So
it's just a matter of providing your specific table name. So every query is going to be
with
regards to some table. And the thing about SQL is every single command is going to end with a
semi
colon. So if I don't put the semi colon, it's not going to do anything. Well, in this case, it
actually ended up working. But that's just because we're using pg admin. If you did
this
in the command line, it wouldn't have worked. So remember semi colon at the end of every
command.
Now let's talk about what this star or asterisk actually does. And so you could see by
default,
we get every row and that's coming from this part of the command. But what does this do?
Well,
if you see here, we get every single column back from our table. And that's because we use the
star
here. However, in an actual database, your tables could have 50 columns, maybe more, they
could have
a ton of columns. And if you're running a simple query to get, you know, some specific
information, you may not want to get all of that data back, you may not care about most of the
columns,
you may only want one or two columns. And so we can filter out what columns we want returned
back
for each row. So the star means I want every single column. So we got every single column that
we defined for the table. But if I wanted to run a query against the products table, but I only
wanted
a list of the different names of each product, I can specify just name. And so what this is
going
to do is this is going to grab just the name column from my products table. So if I run
this
again, you can see we got the same exact data, we got all 13 entries, but it only returned
just
the name column. And then we can add in as many columns we want. So if I wanted the ID column
as
well, and the price, I can hit run. And so now we got the ID and the price. And what's really
nice
is it's going to line up with the order that I write them here. So if I want the ID first, and
then the name and then the price, I could change this to ID, name, price. And so now
it's
going to order it in that in that exact order. So we got ID, name and price. But if you do a
star,
it's just going to return every single column. Now, before we dig any deeper into SQL and
how
to structure SQL commands, I want to talk about capitalization. And so you'll notice that when
I
ran this command, the word select and the word from is capitalized. And I want to make this
very
clear, capitalization doesn't matter. So if I make this lowercase, and I make this lowercase
as well,
and then hit run, you'll see that everything works just fine as it did when it was
capitalized. So capitalization doesn't matter. However, with a any SQL statement, we have two types of
words,
we've got SQL specific keywords, which is part of the SQL syntax. And then we have our user
provided
information. And so let's quickly talk about what those two things are. So the user
provided
information is keywords that I'm inputting in. So I'm asking SQL, or my Postgres
database,
I want the information from my products table. So I'm passing in this data. And so that one,
you just you have to make sure you just match this up with whatever it's called within our
table.
But all of the SQL specific words that I don't create that don't match up with anything in our
database, right, those can be either capitalized or lowercase, it doesn't matter, it's not
going
to impact your SQL statement. But you'll see that best practice is to capitalize it. So we'll
do
capital select and then capital from. And the reason we do that is that we can much easier
tell
which parts of our SQL statement is user provided information. And what parts of our SQL
statement
is just basic SQL keywords. So this just makes it a little bit easier to read. You may not
notice the benefit of it now. But you'll see that these SQL commands can get really long, they can
span
multiple lines. And so having those keywords capitalized, it makes it a little bit easier to
understand. But keep in mind, it doesn't actually impact how the command how the SQL
statement actually runs. So it doesn't matter. But if you want to follow along with what I do,
I like to capitalize them. Now with SQL, what we can do is when we perform a query,
we can rename any of these columns. So if we find that this name is a little inconvenient, and
maybe, you know, renaming it makes it a little bit easier on our back end code,
to better interpret the data, we can pick any column we want and rename it. So let's say we're
trying to fetch the ID column, I'm just going to run this, we can see we grabbed the ID
column,
and it's named ID. But let's say this confuses our back end, because we're retrieving maybe
data
from another database that also, you know, for users or something, and then we also have an ID
field. And we don't want them to get mixed up, I can rename this column. And we can rename it
by
using the as keyword. So I say ID as and then specify the new name. So I can name this
products underscore ID. But now if I run this, we can see that the column is now products
underscore
ID instead of just ID. And I can do this for any and as many columns as I want. So if I wanted
to
rename the is sale column, I can save it as on sale. And so there you go, guys, it's really
as
simple as that. Keep in mind, I kind of continued with my normal SQL format of capitalizing
the
SQL specific keywords just because it makes it a little bit easier to read. Alright, so let's
clean this up a bit, I'm just going to change this back to star so that we can retrieve every
single
column, I'm just going to run that just to make sure nothing's broken. And so till now we've
been retrieving every single entry in the table. And that's because we've provided no filter
criteria,
no filter condition. So let's figure out how we can filter the specific rows that we want
based
off a certain criteria. So let's say I want to match all entries that have a specific ID
of
something, right? And we know there's only going to be one entry that has a specific ID
because the ID field is a primary key. So we know that two different items can't have the same exact ID.
But
you'll see one of the more common tasks when it comes to working with databases is retrieving
a
a row based off of a specific ID. So, you know, a lot of times our background will be like,
Hey, I want you to retrieve the product with an ID of 10. So how do we do that? Easy, we add the
keyword
where then we specify the column that we're interested in, that we want to match on. So
we're
going to use the ID column. And then we say ID equals and then we pass in the value, it's
really
is that simple. So if I want the ID of and so if I want to get the product with an ID of
10,
I just pass in 10. And remember to make sure you have the semicolon at the end, hit run. And
look
at that, we've now got our product that has the ID of 10. And then keep in mind, just like we
did
before, we can filter down based off of just the fields we want. So if we want just the ID and
the name, we can do that. And it should work just fine. We'll change this back and run that. And so
we
got that, if we want to grab the ID of product, an ID of three, run that. And so now we get
the
product with an ID of three. But what's nice about this is that we can choose any column to
match on.
So let's say that I first dump everything so I can just see what I've got. And let's say I
want
to match on any product that has an inventory of zero. So that's basically saying like,
which
products do we not have inventory of so that maybe we can place an order? Well, we can just
say
where. And once again, I'm capitalizing the SQL keywords, I can say where inventory equals
zero.
And so now we've got, we've got a list of products where we need to place an order is really
is as simple as that. Now working with numbers, you just place the number. So any column that
uses
integers or floats or anything like that, you could just say equals that. However, if you want
to match based off of name, the name column, it's going to be a little bit different because
the
name column uses, you know, varying characters or text or, you know, what would be a string in
Python. And so for those, what we have to do is first of all, we'll use the column name
equals,
but we have to wrap it in quotes, single quotes to be specific. So if I want to get the
product with
the with the name of TV, I have to put in single quotes, search for it. And then now we get
that.
But keep that in mind. Because if you remove the quotes, I believe you should get an error.
Yep, and you do get an error. So just make sure when it comes to working with varying characters or
any
kind of text, you want to wrap it in quotes. Now, just like with most programming
languages,
we do have operators. And so till now, we've just been working with the equals operators. So
if I need to get, you know, any product that has a price of, you know, 20, I would just say,
where
price equals 20. And I don't look, it looks like I don't actually have any items with the
price of 20. So I'm just going to do 200. Because I see that the TV is 200. If we run that, it works
just
fine. However, what if instead of doing this, I want to retrieve all of the items that have a
price
of greater than 50? How do I do that? Well, it really is fundamentally no different than how
we do in Python. So I could say where, and I'll say price is, and then we have greater than 50.
So
this is going to grab any item that's basically 51 or higher. And it's not inclusive. So it
doesn't
include 50. And so now look, it looks like it grabbed all of the items that cost more than 50
bucks. But if I wanted to say greater than or equal to 50, I can say just greater than or
equal
to 50. If I run this, we can see once again, that works just fine. But I don't have any
products
that are priced at 50. So I'll try this again with just 80. So we'll say greater than 80. It
should
not return this one because 80 is not greater than 80. But we could see the 80 is gone. But
then if I do greater than or equal to, we can see the 80 is back. And we get the same thing with the
less
than and less than equal to so I could say less than 80. And I could say less than or equal
to
80 as well. And the last operator that I want to discuss is the not operator. So if I wanted
to
grab any product that does not have a inventory of zero, I can say where inventory. And we
have two
different ways of doing this, I can say not equals. So this is kind of the similar syntax that
you see
in some other programming languages, not Python, but other programming languages, I could say
not equals and then zero. This is going to give me every product that we do have adequate stock
for.
And so now we see we get no columns or sorry, no rows with an inventory of zero. But we can
also use
this syntax. So the greater than by the less than and then the greater than, we run that we
should get the same exact result. Alright, so let's move on to performing multiple
operators. So let's say I want to grab any item that we have inventory of. So any any item
that
has an inventory greater than zero, that also costs more than $10. How would we do that?
And
I'm sure you guys can probably take a guess at this, but we'll do inventory greater than
zero.
And then how do we add that next statement where we say greater than I forgot what I said
greater than 20. So we could just say and simple as that we do and and then we pass in the next
criteria,
we'll say and price is greater than 20. Let's try that out. And I realized I forgot the
where
keyword here. So that's why it's throwing an error. And so now we got every single
product
that has an inventory greater than zero and a price of greater than 20. And just like
within
and operator, we also have an or operator. And so if I want to see any product that is
either
greater than $100 or less than 20, I can say where, where price is greater than
100.
Or price is less than 20. And so now we can use two criterias. And as long as it matches
one,
one or both of the criterias, it's going to return that result. All right, let's clean this up
a bit.
And let's say I want to retrieve products with an ID of one, two and three. How would I do
that?
Well, I can do where, where ID equals one. And then we'd have to do an or statement. And
then
we'd say ID equals two or ID equals three. If we run this, it should grab just those three
items.
Now there's another way to perform this kind of operation. And instead of using all of these
ID
equals one, ID equals two, and then just combining them with or statements, we can use the in
operator.
So let me explain how the in operator works. What we can do is we can say ID in, and then we
can
provide a list of values. So I can say, one, two, and three. And once again, I forgot the
where keyword.
And so if you run this, you can see that we got the three items. And you can see that that's
way more readable, instead of having to type out that entire command. And let me show you the
previous
command, I shouldn't have deleted it. So I'm going to use the scratch pad just to show you
guys. And so before we did where ID equals one, or ID equals two, or ID equals three, which one do
you
think is more readable. So both of them do the same exact thing. However, one's a little bit
easier to read. And it cuts down on the number of keywords that you have to type.
Now, before we proceed any further, I'm just going to retrieve all my databases, sorry, all my
entries once again. And what I'm going to do is I'm going to add a few items.
And so all of these items are going to be named very specifically. So I've got a TV items.
And
so I'm going to call I'm going to create one called TV blue. And then let me give this a
price.
And then everything else I can leave default. And I'm going to create a TV red. And then a TV
yellow. And I'm going to save these to our database. And I forgot to give it a price.
We'll just say all of them cost. Actually, let's give them different prices. So
200,
yellow, no one likes anything that's yellow. So I'll give it a value of 50. And we'll save to
our database. Alright, so that's in our database now. And we know how to filter
based off of items. And we can use the where keyword and we can do it for even columns that
are based off of characters, like the name column. So let's say that I want to retrieve
everything,
all of my TV. So this TV, this TV, this TV, this TV, how would we do that? Well, we can't
exactly
use the where statement because I can say where, name, and then I mean, we have equals we
have
greater than we have less than, but none of those really do what we want to do. So how can we
match
just on all of our TVs? Well, we can use our like operator. So we could say where,
name,
like, and then what we can do is we can pass in something very specific. So we pass in the
text
that we want, but then we can start using things that are kind of like regular expressions,
like we have in Python. So I could say TV, and then percent sign. And what this is saying is
that
I want to grab every row that has a name that starts with TV, and then the percent sign
means
any random characters afterwards. So this is just saying I want any product that has a name
that starts with TV. So if I run this, you can see that we were able to grab all the TV. So it's
very
similar to read regular expressions. And if I remove this, and instead just add, you know,
add
in the letter A, this is going to return all of my products that start with the letter A. And
I don't have anything. But if I change this, I know I have one product that starts with an R. So if
I
try that, you can see that we get the remote. And we can also flip this so I can say, you
know,
percent sign and then pass in some letters at the end. So if I want to grab anything that ends
in the letter n, I can run that looks like I've got nothing that ends in the letter n, maybe
E.
And so now we get all of the items that end in an E. And then we also have the ability to do
the opposite. So this matches anything that ends in E. But if I want to get every line that
doesn't
end in any, I can say not, but that's just going to give me the opposite. So that's going to
give
me every row except for those two rows. And what we can also do is if I want to get any line
that has the letter en somewhere in the name, I can do the percent sign before and after. And so
we
just look for these two characters anywhere in the name. So if I run this, and it looks like I
got an error. And I'm not sure what happened there. But I changed this, I just delete it and
retype
it. And so now it no longer gives me an error. So I'm not really sure why that happened. Could
be a bug of some kind, or maybe there's a typo I didn't see. But this is going to grab
any text that has the letters en and then it can have any characters before any characters
after.
And then since we have the not operator, then it's going to do the exact opposite. But
then
I can also remove this so I can grab any lines that match just this. And then it looks like
once
again, I get the same exact error. Okay, there we go. Now, when we make a query to our
Postgres
database, we can also specify how we want to order the results. Now, by default, it looks like
it's
just going based off of the ID, but it's not necessarily going to be like that. And so
it's
better to actually tell Postgres how you want to order the products. So in this case, I've got
no filter criteria, I'm just returning everything. And let's say I want to filter or order
the,
the products that are returned based off of price. So if I say order by, I can then
specify
the price column. So let's see what happens when I run this. All right, we could see that it
looks
like it starts at the lowest price and then works its way up. So by default, when you specify
the order by keyword, and then specify the column, it does it in an ascending order. And so you
could
specify the order at which you want to go the direction, I guess by saying a SC. So this
means
ascending. So this shouldn't change anything, because Postgres by default is going to always
do it in an ascending order. So if I run this, nothing changes. However, if I want to do
this
in a descending order, I can say DSC for descending. So now it's going to start at the most
expensive
price and then work its way down. Now let's change this. And let's say we want to order by
inventory. And I want to see which products we have the most of. So I'll say
inventory,
and then descending because we want to start at the highest value and work our way down. So
I'll run this. And then we can see it starts at the highest one and then works its way down. But
we've
got a whole bunch of zeros. And let's say that between these guys, I also want to specify a
tie
breaker of some kind, we can also specify the order using a second column for these
tiebreakers.
So I can pass in another column. And so let's say that anytime there's a tie, I wanted to then
order it by, let's say price. So I'll say price. And then we'll say the cheapest price. So
ascending,
now ascending is always the default. So we can just delete that keyword. And so let's run this
now. And I forgot to put a comma. So after each column that you want to sort by, you have to do
a
car. And so now we got all of our data. And so let's take a look and see if this actually
worked. So we can see we're sorting based off of inventory, then we get down to zeros. So these are all
tied.
So we take a look at the second column that we pass. In this case, we're looking at the
cheapest price. And so if we see the first one, looks like the cheapest price is two, then it goes up to
four
1030 4050 8100 200. And you can pass in as many columns as you want. For your sorting
criteria,
you don't have to just stick with two. And it's really just a matter of doing a comma, and
then passing in the next column and then whatever the criteria is. And you'll see that
one of the common things to do is let's say we want to get the most recent products.
Well,
I think it makes sense to just sort based off created. So that's a timestamp for when the
product was created. So let's see what was the most recent products that were put into our
database,
we can just say, created underscore at and what order do you think we need? Do we need
ascending
or descending? If you don't know, think about it. And if you still don't know, test it out.
That's the easiest way to find out. So we can see that the first one is remember, this is
always
going to default to ascending. So when it's ascending, we can see that there's a one at 820 is
created a 20. And then the bottom ones are created a 21. So it looks like this starts
off
with the oldest one, and then works its way up to the most recent one, because that has a
later date, you think of the later date as a higher value. So if we want the you know, the most recent
products
first, we would do descending. And so now we've got the most recent products. And with this
order,
with the sorting property, remember, we can just chain this on to any SQL statement. So if I
also
wanted to, you know, get all of the products that have a price of greater than 20, and then
sorted
by something, I can just specify my where keyword and say where price is greater than 20. And
then
for all of those results, I want to order by who was most recently created, we'll search that.
And
so there you go. And so that's the great part about SQL is you'll just keep chaining
these
different SQL keywords to filter out the exact data in the exact order that you want. Now in
an
actual production environment, our tables could have millions, tens of millions, hundreds of
millions of rows. And so it would never make sense to just perform a query like this, where we
just
dump everything, because then all of a sudden, we're asking our database to send back 100
million rows. And that's just unacceptable. And our software that we're using to even send the
query
may not even be able to handle that. And so usually you want to provide a limit of some kind.
So you may want to say, Hey, I want to grab, you know, the first 10 rows that match this criteria.
Well,
we can do that by using the limit keyword. So we say limit, and then pass in the number of
rows that you want. So if I run that, you can see that we get 10 rows, I lower this to five, we can
get
this, we can get five rows. And you can chain this on to any SQL expression. So let's say I
wanted
to grab all of the products that have a price of greater than 10. If I search that you can see
I
get 10 results. And let's say I want to limit this to only the first two results. So if I hit
that,
you can see that now I get two results. So all the keywords that we're covering, including the
order by, and any of the filter expressions, we can just keep chaining on to our SQL statement
to
further specify exactly the data that we want. Now with limit, there's another keyword
that
I want to cover. And so let's say that I want to grab all products, and I want to order
by
ID. Alright, so it's gonna return it like it normally does. And it's ordered by ID. And let's
say I want to set the limit to be five. Now we get products with an ID of 123, five
and
seven. Do I not have a ID of six? I'm just curious. Actually, so let me cut this out for a
second. I
just want to see if there's an ID of six. There isn't. Okay, so that's that's expected. So let
me add that back. So we'll say I'll do order by ID, and then we'll limit to five. So we have
the
first five. Now let's say that we want to skip a certain number of rows. Because every time
we
search this, we're going to get the same result. And let's say that we don't actually care
about the first two results. And I want to skip past them, we can provide an offset. So I'll do
offset
two. And so what that's going to do is it's going to skip past these two first two results and
then give us the next five. So we should see the first result be remote, and microphone and car and
then
the two after that come after that. So if I search this, we can see that that first one is a
remote. And the last one is the pencil sharpener. And then if I do an offset of, you know,
five,
it's going to skip the first original five. And so then we get pencil pencil sharpener and
keyboard. So you're going to see that, you know, when it comes to limit and offset, these are going
to
come in handy. When we start implementing pagination in our API, we're going to definitely
make use of the limit and offset keywords. Till now we've been using PG admin to create new
entries
in our database, by just going into the little GUI here and then just adding new values.
However,
when you're actually working with the Postgres database, you're never actually going to do
this, this is more for administrative purposes, but a real application is going to use
SQL,
just like we use SQL for making queries, we're going to use SQL for adding new entries. So
let's
take a look at what the command looks like for adding a brand new entry into our database. The
command starts with an insert, because we're going to insert a brand new row.
And then we say into and we're going to specify the table that we want to add a new entry to.
Now in our database, we only have one table, so it's going to be products. But here you
would
provide whatever table you'd like to add a new entry into. Then we have to do what we have
to
do is provide a list of columns that we want to provide data for. And so I'm going to leave
this
blank for now, we're going to come back to that. And then we specify values. And then
here,
we're going to actually provide the values for each column. And so if we take a look at our
products database, we know that the name is required, we know the price is required.
And
then that's about it, right, we can choose to provide an S sale, but it's going to default to
false. And then we can choose to provide an inventory. If we want to, if we don't,
though,
it's going to default to zero. So we have to provide a name and a price. And it's ultimately
up to us to decide what we want to pass in. So we know we need a name. And we know we need a
price.
Now, let's say we're going to let Postgres automatically give us the default is sale value to
be false. So we're going to leave that out, because we're not going to pass in any data.
So
this column is just for all the columns we want to pass in data. But let's say we want to give
it a custom inventory value, instead of just defaulting to zero, here, we would
provide
inventory. And then in the values column, these are going to be the values that you want to
actually provide for the name, the price and the inventory. So let's go ahead and provide that.
So
let's create a brand new item. So let's give it a name. And the order in which you pass things
has to match up with the order that we pass in here. So the first column that we're going to
provide
a value for is named because it's the first one here. So what name should we give it and keep
in mind, since it's text, or varying character, we're going to have to put this in
quotations.
And let's add in a tortilla. So now we're a grocery store. Right, then the next one is
going
to be a price. So how much is our tortilla going to cost? We'll say it's $4. And then finally,
we have to provide an inventory value. So let's say we're going to have 1000
tortillas.
And then as usual, you want a semi colon. All right. And so at this
point,
this is all we have to do to create a brand new entry. So let's hit run. And let's see what
happens. So query returns successfully. And so we see this insert 01. And so at this
point,
it says, insert 01, and then query returns successfully. So did we actually
successfully
insert that something into our database? And more importantly, what exactly does this mean? So
if you see insert 01, that means everything worked perfectly, there was no errors. What
this
means is that first of all, ignore the zero. So this means that we're using the default
postgres
configuration. So this represents the OID. But I believe postgres by default doesn't use that
unless you specify it to so it's going to give a value of zero. And don't worry too much
about
that. That's kind of outside of the scope of this course, we're not going to mess with that.
But instead, what we want to focus on is that second column. So this means that we
inserted
one row. So that means our insert worked perfectly. So let's take this. And what we're going
to do is
first of all, I'm going to copy this, cut it out, actually, please paste it into my scratch
pad for
now. And then we're going to say select star from rows. So I select star from
products.
I'm going to run that. And so if we go to our bottom, we should see our brand new
tortilla.
And so we provided the tortilla value, we provided the price. And we did not provide an is
sale. So a default false, and then we provided an inventory. Now, if I copy this
again,
and paste it back into here, right, and if I move the name column,
right after the price, well, now SQL is expecting me to provide the value for price first,
then name
then inventory. So then I would have to take the price, move that over to the first
column,
and then move that move that into the first entry. And then we have the name, and then the
inventory. And so keep in mind, the order here has to match the order here. So once again, I'm going to
go
ahead and just add this again, because why not? And so now, oops, it looks like we have an
error, what happened here? All right, there we go, I guess we just needed that space. And so
now
it's in there. And you'll notice that once again, we get the zero one, so that means
everything was inserted properly. So now we should have two items in our database with the name of
tortillas.
And when you're working with, with Postgres, and especially with an API where we want to
create a
new post, like in our application, the general convention in an API is once we create the
new
post, we want to return that data back to whoever sent that request to the API. So we want to
get
that brand new brand, newly created post with the new fields like the created ad field and all
the
default values, and send it back to the client or the front end. So how do we get Postgres
to
automatically return it? Because right now, it doesn't seem to do that. And we could, you
know, just provide two statements, right, I can add in a second statement that says, select star, you
know,
from products, this is going to dump everything we could potentially, you know, filter on, you
know,
where name equals, you know, tortilla, or something like that. And then at that point, we
should have
we should have three entries. But that's one way of getting back the result. However, there's
a much easier way within Postgres. So first of all, I'm going to change the name, add a new
item,
and say this is a car, and I'm going to give it a price of 10,000. And what we can do is we
can
pass in a keyword called returning. But this is going to return the newly created item or
items,
if you want to insert more than one item. And then here, we have to specify the columns that
we want for the newly returned items. So if I do star, that's going to return every single column.
If
I do ID, this is just going to give me the ID of the brand new brand new created item, I'm
going
to return every single column. So if I try this now, look at that. So it creates the new
entry,
we can see that has an ID of 26. And it gives back the entire row. Now, if you want to insert
more
than one row at a time, what we can do is we just do a comma here after values, and then just
provide
the data for the next row. So we'll do, we'll create a new item that costs $50. And this
is
going to be a laptop. And then we'll say there's going to be an inventory of 25 of them. And
then
if we wanted to add another one, and we could just do another comma, and then add some more
values
in. So we'll give this a price of 60. This is gonna be a monitor. And then we'll say we
have
four of those. And so here, we can go ahead and run, you can see that it added three
items,
and then it returned all three, because we have the returning keyword. And if you wanted to,
we could just say I want the ID, we can just say I want the ID and be created at field, maybe
the
name as well. I run this, we could see created another three new items, and then return
just
those three fields. And so that's pretty much all I wanted to cover when it comes to creating
new
entries in the database and inserting new rows into a table. Alright, so in the last
lesson,
we learned how to insert new entries into a database, let's now figure out how to delete
entries. So let's clear out this query real quick. And the command to delete a entry from a
database
is going to be delete, who would have thought right, delete, then you say from and then
you
provide the table that you want to delete a row from. So we'll say products. And then we want
to specify condition. So what is our match criteria? What rows do we actually want to delete? We
have
to tell SQL I want you to delete this specific row. So what do we usually match on? Well, if
you take a look at our API, are specifically our delete endpoint, the user provides the ID
of
the product they'd like to delete, or I guess in our application, it would be the ID of the
post we'd like to delete, but we're working with products. So we'll say, you know, just like
with
a regular select query, you say where, and we can say ID equals and then the ID of the product
we
want to delete. So let's take a look at one of these products, I'll grab a product 10. And
then let's run this. And then afterwards, let's actually do a select star from
products,
so we can see all of the products afterwards, just to verify that the ID of 10 or the product
with an ID of 10 got deleted. So you can pass in as many SQL commands as you want here. So
we'll
run that. And so it ran. And then we can see that we the product with an idea of 10 was
deleted.
Now, I'm going to change this to 11, because there's no longer a product with an idea of 10.
So I'm going to delete this product. But what we can do is we can tell Postgres to
actually
give us back the specific entry before is deleted. So we can see what that looked like. So we
could
just pass in the returning keyword, just like we used with the insert statement, and then we
specify the columns we want. So I'm going to say all of the columns. And then if we run
this,
we can see that it deleted the product with an ID of 11. And we can see what that product
looked like.
And keep in mind, if I don't pass any of this data, and I just run this, right, you'll see
that it says a delete of one. So this is just telling you how many total rows
are deleted. Now, let's say that we want to delete multiple products based off of a criteria.
So let's
say we want to delete any product that has an inventory of zero. How do you do that? Same
exact
command, we just have to match on any product with an inventory of zero. So let's say,
delete
from products table, where and then we'll just say inventory equals zero. So this should
delete
every single product with an inventory of zero. We run this, we can see it deleted eight
rows,
let's just do a select star from products. And you can see we no longer have any products
with
an inventory of zero. Alright, so we saw how we can insert new data into our table. And we saw
how
we can delete entries from our table. I think the next logical step is to figure out how to
update a pre existing row. So the command for updating is going to be, as you guessed, update. And
then
we have to specify the table that we would like to update. And so in this case, it's going to
be the products table. And there's gonna be two things that we have to pass in. So first of
all,
we need to specify which entry we want to update, we can update more than one entry at a time.
But you know, just like when it comes to deleting, if you take a look at our API that we built
so
far, the user provides the ID of the specific post they want to update. So normally, we're
going to update based off of the ID column. So we'll say where, you know, just like the select
statement
or the delete statement, we can say where ID equals and then the specific product of the
ID,
and the specific ID of the product that we want to delete. So let's say we want to delete one
of the tortilla. So an ID of 25, sorry, not delete, but update. So let's say 25. Then we want to
pass
in all the columns that we want to update. So we pass in the name of the column, and then what
the new field should be. So let's say a name column, I want to rename this to be flower
tortilla.
And then let's say I also want to update the price. So I could say the
price
equals and then 40. So right now the price is four and the name is tortilla, it's going to now
be flower tortilla and 40. I run this. And don't forget the semicolon at the end.
And I realized I forgot one keyword. So before you pass in all of the new
values,
we have to use the keyword set. So it's update products, then we want to set these
columns,
then I run this. And we can see that we updated one field. And I'm going to copy
this,
put it in my scratch pad for now, and just do a select star from
products.
And so if we take a look at our tortilla, so now it's called flower tortilla, we can see the
ID
is still the same because we didn't change that. And the price got changed. However, all the
other fields have all remained the same. So that's good. But just like when it comes
to
inserting new data, we generally want to return the newly updated product to the user. So how
do
we do that we can just use the returning keyword, I'm going to copy this again. And let's
update a
different product. So I'm going to update the car, which is a ID of 30. And let's just say I
want to
update one field. I'll remove all these and I'll just say, I'm going to update the is
underscore sale field to be to set it to true because right now it's set to false. We'll set that to
true.
And then we can say returning star. If we update this, we can see that we now got the car, we
can
see that the is sale set to true and it returned that data and it was successfully able to
update that. Now, let's say we want to update multiple rows. And let's say I want to update, we'll
just
let's just update everything, right? So if I don't pass in a specific condition, I can update
every column or I can specify a condition and then update just those columns. So let's, let's
say
I want to match any, any product with an ID of 15 or greater, so an ID of greater than
15.
And I want to set the is sale to be true. If I run this, we can see all of the entries that it
was
that it updated. So now all of these entries, which all have an ID of greater than 15, have
all all have the is sale column set to true. And so that's how we update entries within
a
Postgres database. At this point, I think we have a good enough foundation when it comes to
working
with Postgres, we're able to successfully query a database insert new rows, delete rows and
update
rows. So I think it's time we finally started to go back to our code and figure out how we
can
actually work with a database within a Python or a fast API application. But before we do
that,
I want to do a little bit of cleanup. So with our database, we no longer need our products
table.
So what you can do is you can go ahead and delete it. So you can just right click, and then
hit delete or drop. Technically, if you don't want to delete this, and you want to
keep
it for now just for reference, you don't have to delete your table, it's not going to impact
anything else. So you can keep it. But I'm going to go ahead and delete it because we don't
actually
need it anymore. We'll just do that hit yes. And then it's going to delete that and we should
no longer have tables. And I'm also going to move over to another machine. So when we go to
the
other machine, you will see a whole bunch of other databases. But those are all databases for
my other project to make sure you just focus on the fast API database. All right, I'm back
over
to my new machine, you'll see that if I take a look at my database, there's going to be a
whole bunch of other databases. But I've got my main fast API database. So that's what we're going
to
be working on. Don't look at any of these other ones. And within here, it's going to be pretty
much empty. I've got no tables. And so we're going to go ahead and create our table for
our
social media application. So, you know, we've been working with posts. So I think it makes
sense to create a table for posts. And before we do that, we have to figure out exactly what are the
columns
for our specific table. And to do that, I think best to actually take a look at what our
application
looks like at the moment. And so if we actually take a look at a structure of a post, we know
that it's going to have a title, we know it's going to have a content, we know it should have a
attribute
called publish to determine if it's published or not, we're going to get rid of this rating
that was just for demonstration purposes. So I'm going to remove that for now. So it should have a
title
content published. And like any other item, when it comes to a database, we're going to have
an ID
column as well so that we can we can uniquely identify each entry. And then we're also going
to have a created ad field to see to track when the specific post was added to the
database.
So we've got essentially five columns. And then once we have that, we can go ahead and just do
what we did before, we can go to tables, we can hit create table. And then we're going to
call
this table post because it represents our social media posts. And then we'll go to the column
section and start defining our columns. The first one's going to be our ID column. And so with
our
ID, we want to do serial. So this is going to cause it to be an integer that automatically
increments for us. And that's going to be our primary key, as it was before. We'll add
another
column for title. This is going to be a varying character or character varying. Not null is
going
to be set to true. We can't we don't want to be able to create a post without a title. Then
we're
gonna have a column called content. I believe that's what we called it here. Content
Yep.
Once again, this is going to be character varying. And once again, this is going to be not
null.
Then we're going to have a published column. And this is going to be a Boolean. This is also
going
to be not null. However, I'm going to provide a default value. So if I do leave it
blank,
it's going to default to true. And then finally, we're going to create the created column.
And
this is going to be a timestamp with timezone. This is also going to be not null. And then
just
like we did before, with our products, we're going to add a constraint. But that is going to
be now so it's going to grab the current time whenever we add this entry, and it's going to
automatically
add it to that column. And so at this point, we've got our tables defined, we can go ahead and
hit
Save. If we right click on posts, and then go to view and edit data, we should have
essentially
what is an empty database. And for now, we know how to insert data, we can do it either
through
SQL, or we can just do it to here, I'm just going to do it through here, just because it's a
little bit quicker for now. So I'm just gonna say this is my first post. And the content is, we'll
say,
interesting stuff. So just go ahead and just create a couple of posts. And then I'm gonna add
my second post. All right, and then we can just hit Save. And so now we have
two posts in our database. So we can start working with that. When it comes to working with a
Postgres
database within a Python application, we're going to need a Postgres driver. And so there's
going to be a couple of different libraries that going to do that that can do that we're going to use
this
library right here. And I believe in a couple months, a version three is coming up. But
right
now, this is the latest version. So head on over to the documentation, and it's going to show
you how to just quickly set this up. And so if we go to basic module usage, this shows us how to
set
up a connection to our database. And so we're going to import the library. And then we're
going to set
up a connection. And hopefully, this is big enough for you guys. So you just say connection.
And then here we pass in all of the data for our Postgres instance. So you know, what's the IP address
of
the Postgres database? What's the Postgres database that we want to connect to? What's the
port number things like that. And then we set up a cursor. So this cursor is what we use to actually
execute
SQL commands. And so you could just do cursor dot execute. And then here we just say, you
know, create table. Well, you guys aren't familiar with that command yet. But you can do you know,
insert
into you can do select star from test. And then at that point, we can then do fetch one fetch
all
and then if you ever want to make changes to a database, you just do commit. So it's a it's
fairly straightforward. So let's go to our code and set this up. And so the first thing that
we
want to do is we want to install that library. So we'll do pip install. And I'll just copy
that name.
All right, and now it's successfully installed. So let's set up our connection. And so a
connection to a database can fail, right? Maybe the database is unreachable,
maybe the database is down. There's a lot of things that could cause issues with us being able
to connect to it, we could put in wrong passwords or something like that. So anytime
you
have some kind of code within Python that could potentially fail, we're going to use the try
statement. So I'm gonna say try and then we're gonna say connection equals. And then I
realized
the first thing that we have to do is we actually have to import the library. So let's just
copy this line right here. And then I'm gonna say p s y c o p g two, I don't know if there's a specific
way
to actually pronounce that that's why I'm not trying to pronounce it. And then we call it the
connect method. And so here we have to pass in a few properties. So the first property is going
to
be the host. So that's basically the IP address. We also have to pass in the specific
database
we want to connect to. We also need to pass in the username that we want to connect as as well
as
the password. And for now, we'll just keep that as such. So let's fill in these fields. So
what's
the host. So since this host is just our local machine for the IP address, you could just say
local host, that means our own IP address, I spelled a misspelled database. And I'm going
to
set this to be Postgres, because that's what's sorry, it's not going to be Postgres, it's
going
to be fast API, right. So that database is just going to match up with the name of our
database right here. username, we've been using the default Postgres username, and then
password.
I'm guessing you guys could have guessed what my password is, it is password 123. And then
finally, there's one extra thing that we have to pass in. And so this library is a
little
bit weird. So when you actually make a query to retrieve a bunch of rows from the database, it
doesn't include the column names, it just gives you the values of the columns, which, you
know,
it's like, I don't know what, you know, what value map to what column, so you actually have to
pass in an extra field to get the column names, which kind of seems dumb, but that's the way the
library
works. And so what we're going to do up here is we're going to import something else, we're
going to
do import, same library dot extras, import, and then real dict cursor. And this should be
from
and then the last property we're going to pass in here. The last argument is going to
be
cursor factory equals real dict cursor. So like I said, all this does is it's also going to
give
you the column name. It's going to give you the column name as well as the value. So you know,
which value map to what column or then it's just trying to figure the order and then mapping it
to
comms gets a little complicated. So this will just make it a nice Python dictionary when it
returns it. All right, and just like it had in the, the documentation, we'll say cursor equals con
dot
cursor. So it's just calling the cursor method and then saving it in a variable named cursor.
And so
all this is going to do is this is what we're going to use to actually execute SQL statements.
And if
it's successfully able to connect, we're going to say print, and then we're just going to
print out something like a database connection was successful. However, if we weren't able to connect to
it,
and we get an exception, we'll say accept exception as error. So we're going to get the error
stored
in a variable called error. And then we can just say print connecting to database failed. And
then
after that, just for our knowledge, we can say the error was error, we'll just print out the
error.
Okay, and you can see that we were successfully able to connect to our database. So it looks
like everything worked well. But just as a quick test, I'm going to change my password
here,
I'm going to put it as an incorrect value. So let's save this and let's see what happens.
Right, we can see that the password failed. And so at this point, what the code does is it
failed,
it printed out the error, and then it just keeps going through the rest of our code. So then
it starts up our fast API server. But at this point, you know, our application is not going to
work
because the Postgres database connection failed. So really, our application, our API, our web
server
can't do anything until we get a connection to our database. So just having it fail, and then
kind
of gracefully handling that error doesn't do anything, we need to wait for that
connection
to actually go through before we do anything else. Or then at that point, there's really no
point in having our server up and running if we can't access our database. So what I like to
do
is we're going to set up a while loop. I'm just gonna say while I want this loop to just
continuously
run until we successfully get a connection. So we'll say while true, which means we're just
going to keep doing this over and over and over again until we break out of it. And I'm going to
tab
everything over and put everything in that while loop. Alright, and so while this true, if
we
successfully are able to connect to the database, I'm just going to break out of the while
loop.
However, if we fail, then it's just going to go right back into that
loop.
And so now, one other thing is it's going to do this really quickly. And I would like for
after
an error, I would like it to kind of wait two to three seconds before it tries to reconnect.
So what we can do is we can import the time module, I'll say import time. And here, I'll just say
time
dot sleep. And I'll sleep for two seconds, use whatever time you want. And so now if I hit
save,
check out what happens. So it failed to connect because of wrong password, and then it's just
going to keep retrying every two seconds. Now for a failed password, it's never going to
connect.
However, if it's an issue with your internet, if it's an issue with the database hasn't having
not fully initialized, then having it just kind of redo this until the database fully comes
up,
is a nice way of kind of handling that. So if I change this now back to the correct
password,
we see that we're now successfully able to connect. So this is our code for actually
connecting to
our database. And at this point, we can start working on actually writing our SQL code
and
then you know, being able to manipulate our database from our fast API. And I do want
to
point out one thing. Generally, when you're working with your code, what we did right here is
very
bad. We hard coded all of our database information right into our code. This creates a
problem
because first of all, when we check this into Git, now our database password is stored in
there. And then we run into some extra issues, because this is the connection for our
development
environment, or our development Postgres server, our production Postgres server is not going
to be running on a local host, maybe the database might be called something else, the username and
the
password are for sure going to be different. So if we hard coded in, we won't actually be able
to change it in the future, we need a dynamic way to kind of have our code change based off of if
it's
in our development environment or our production environment. So later on in this course,
we'll figure out how to do that. However, for now, we're just going to work on keeping things
simple,
and just learning how to interact with the database before we start moving into things like
environment variables. Alright, so the first thing that we're going to work on is
retrieving
all of our posts from our posts table. And so what we're going to do is we'll go to our
specific
path operation for that, which is the app dot get slash posts right here. And let's figure out
how to do this. So if we go back to up to our connection, right, you'll see that we have
access
to this object right here, which is cursor. So we're going to use that to actually make a
query, we'll say cursor dot execute. And then this is where we are going to paste in our SQL
statements.
I'm just going to put in three quotes right here. And then we just put in our SQL statement.
So in this case, right to retrieve all posts, we just do select star from
posts. Okay, and let's save this as a variable. So I'll just say these are my
posts.
And then let's just do a print post to see what we get. And so I'm just going to go to my
postman. And we'll just find my get posts request,
we'll send it. Alright, and let's see what happens, right, we could see that it printed out
none. So
this doesn't actually do anything, right? This is just passing in our SQL statement.
However,
to actually run it, we have to do cursor dot, and then we have a couple of
options,
we can do fetch all fetch many fetch one. So when it comes to retrieving multiple
posts,
we're going to always use fetch all don't worry about fetch many, I don't think we're ever
going to use that. And then if we ever want to find one individual post, you know, like finding a post
by
an ID or something that we can use fetch one, because I believe if you want to fetch a post by
an ID, and you do fetch all, it'll technically get it, but it'll just keep searching
through
the database for another post with that ID. However, we know that only one post can
have
that exact ID. So it's inefficient to kind of search through the entire database when we know
there's only ever going to be one result. So for those, it's always better to use fetch one.
But
since we're retrieving multiple posts, we're going to use fetch all. And at this point, you
know,
we don't actually need to save this in a variable because it's never going to return anything,
we can save the output of this to be posts. And now if we save this, run this again.
And I realized I forgot to actually call it. Let's try that again. Right, you can see
that
we got all of our posts from here. And so now instead of returning my post, which is our old
array, which we're not going to use, we're going to return what we just fetched. So we're going
to
return posts. And we'll go rid of that print statement, it's not needed anymore. And
hopefully
this works. So let's hit send. Alright, and so now you can see we've got our two posts from
our
database. And it's got all the extra fields. So it's got the publish, which is the default
value,
and then it's got the created at timestamp, which post has added. So now we've actually
successfully been able to retrieve posts from our post guest database. And you can see how easy it is
to
actually work with that. In fact, it was actually a little bit easier to do this than to
actually work with a just an array stored in memory. It really just comes down to understanding
SQL.
And since we spent, you know, the last half an hour or so really drilling into how SQL
works,
you'll see that SQL is pretty easy. And then once you know, SQL, you know, fetching, you know,
retrieving, updating, deleting posts from a SQL database, even within your Python code is
dead
simple. Okay, so let's now move on to creating a brand new post. And so if we just quickly
take a
look at the code that we have right here, you can see that we have our schema, we're going to
save
that in our pedantic model. And so we will be able to access the properties from the body
within this post object. So I'm going to clear this out and just delete all of that code for
now.
And for now, I'm just going to hardcode some value, I'll just say created post. And so let's
work on inserting a new post into our database. But once again,
we're going to access the cursor object, we're going to do cursor dot execute. And then
once
again, we're going to use the three quotes. And we're going to use typical SQL. So to
insert
something into our database, we do insert. And then we say insert into what table do we want
to insert into, we'll say the post table. Right. And then here, we have to pass in the
columns
of all the fields that we want to enter in. So if we go look at our database, that we have to
pass in a title, and a content. And then everything else is optional. So we can
also provide published as well. And then did the database creates the created that in the ID.
So it's really just these three columns that we're going to pass into here. And so it's going to
be
title content, and then published. Yep. And then we have to pass in the values for those
columns.
And this is where things get interesting. You never want to do any kind of
string
interpolation and then pass in the values of post directly into this. Instead, what we want to
do is we're going to parameterize or sanitize all the data that we put into the SQL statement. So
what
you should do is do percent s. And so this kind of represents a variable. And we're going to
do
percent s and then percent s. And so this variable is going to be the value that gets passed
in title,
this variable variable is going to be the one that gets passed into content. And this variable
is the one that gets passed into the published column. And what we want to set these as
we're
going to actually use a second item that we pass into the execute command, the execute method.
And
so here we pass in the actual values. So this first percent s, what do we want that to
be,
that's going to be post dot title. So we're grabbing the title from the body, then we
want
to pass in post dot content. And then we want to pass in post dot published. Alright, and this
may
seem a little confusing. And let me correct that. Because I'm sure a lot of you thought we
could just do cursor dot execute. And then here we pass in just in a NF string. And then I can just
copy
all of this and then pass in here. Oops. And the values we can just do, you know, post that
title
and then, you know, post content and so on. And this would technically work. However, this
makes
you vulnerable to SQL injection. So if the user for the title decided to, you know, if we go
to
the create post, and for the title, he decided to pass in some kind of weird SQL statement
like insert into blah, blah, blah, right, he puts in SQL instead of a valid title, this is what is
a
SQL injection attack, and he could potentially manipulate data within our SQL database.
That's
why it's never good to do it to pass that data in directly into here. Instead, all of
these,
you know, SQL libraries like, like our Postgres library here, they actually can sanitize
the
inputs. So when we do this percent s method, and then pass it in as the second field into
the
execute statement, it'll actually make sure that there's no, you know, weird SQL commands in
there, and it's going to make sure that we're not vulnerable to SQL injection. So that's
why
we never do it like this. And we always do it like this. And just keep in
mind,
right, these are just variables or placeholders, and then the values we want to pass into that
will be passed into these parentheses right here as the second parameter into the execute
method.
So post dot title, since it's the first one in this list, is going to go to the first percent
s, and then post dot content is going to go to the second percent s and then post dot
published,
we'll go to the third percent s. But the order matters, they match up with whichever percent s
comes first. So if I took, you know, post out published, right, and then moved it all the
way
to the front. Well, now post out published would go to the first percent s post out title
would go
to the second one, that would cause issues because it doesn't line up with that. So that's why
we want to make sure that the the order really does in fact matter. And just like we had before,
anytime
we create something in our database, we want to return the created result. So we can use
the
returning keyword. And we'll return everything. And so you're probably thinking, you know, we
can
just save new underscore post equals that. And then since we're returning it, it's going to
get
stored into that variable. And that's not exactly correct. Instead, what we have to do is to
get that returned value, we have to do a new underscore post. And then we do cursor dot batch
one.
So that'll get whatever we return from here. And then what we're going to do is we're going to
say
new underscore post is going to get returned to the user. Let's save that. And let's see
what
happens. So go to a create post. And then I'm gonna clear that out and just say, Hey,
hey,
this is my new post. And let's hit send. So it looks like everything worked, right? 201
created,
you can see it's got all of these values. Let's go back to our SQL database, I'm going to just
do a quick query. So I'll just say query tool. And I'll just say select star from posts. Let's run
it.
And if we take a look at the values, and the title should be, hey, this is my new post with an
ID of three, you could see it's not in there. And so you might be wondering, well, you know, the output
of
our API looks perfect, it sent it all back. Well, when you're working with Postgres, or any of
these
libraries, you have to actually do one last thing, you actually have to commit the changes.
Just like
when it comes to working in our database, you know, if I create something here, passing some
data into
the content, it's not saved in there, I actually have to finally save it. So to save that
data,
we have to reference, instead of the cursor, we're going to reference the connection, and then
we
have to do a commit. So if we go down here, we just say con, which is my connection to my
database,
and just say commit, so that's actually going to push those changes out. So we'll save that.
Now
I'll try this again. Alright, and so now we got it looks like it has an ID of four. If I go to
my
Postgres database, I'm going to do another search. Sure, okay, if it deletes it. And so now we
can see
idea for Hey, this is my new post. So now it works. So keep in mind, anytime you want to
insert data
into your code, make sure you do a connection commit to actually save it into the Postgres
database, because these are all staged changes. So we're staging it, we can see the result of
the
stage, but we have to commit it to the database to actually finalize those changes. Alright,
so now let's move on to fetching an individual post by an ID. I want you guys to take a crack at
this,
see if you can try to figure it out. It's going to be relatively similar to the two other
previous ones we've done. So we'll start out by referencing the cursor object as usual, and then we'll
execute.
And then here, we're going to pass in our SQL statement. And so we're going to do a select
star
from post, that's going to grab every post, but we want to grab just one post based off of the
ID. And so you guys already know how to do that, we can say where ID equals, and then whatever
ID
that you want. And so let's take a look at our database, I have an ID a post with an ID of
one.
So right now, I'm just going to hard code it, I'm gonna say post with an ID of one. And at
this
point, this is not going to return anything, to actually return something, we'll say, cursor
dot
fetch. And then we're going to use not fetch all, but fetch one, because there's only ever
going to be a post with an ID of one, just, and so this is going to make it a little bit more
efficient.
And then I'm going to save that as post. I'll just say test post not to get confused
with
the other post. And we're going to print that out just to see if it worked. So let's get one
post.
And it doesn't really matter what we send it as. If I take a look at this print
statement,
you can see that we were able to get just that one single post. So it looks like that worked.
So now let's clear out all of this other code. I'm going to remove the print statement.
Actually,
before we do that, first of all, let's not hard code the value anymore. Instead, we want to
use the ID that we passed in from the path parameter. So how do we do that? Remember this, we want
to
always make sure that we're not vulnerable to SQL injection attacks. So we'll do percent s,
that's going to be a placeholder for the value. And then the second thing that we pass into
the
execute method, this is going to be the ID. So we take the ID, and then that's going to get
pushed
into this value. So it's essentially doing the same exact thing. So let's see what happens
when we try to run this again. And I'm going to try to grab a post of one. And let's see what
happens.
Alright, so it looks like we got an error. So let's take a look. And it says that int object
does not support indexing. So it's not very helpful. However,
I'll tell you exactly what's happening. Because this is a string, we need ID to be a string
as
well. Right now it's an integer because we validated as an integer and we converted it
to
an integer. But we need this to be a string or then it's not going to work. So we have
to
convert to a string. It's pretty simple, you just do string. And that's all and at that
point,
that should fix the issue. And I'm sure you guys are a little bit confused. Because when it
comes in as a path parameter, it's going to come in as a string, we then convert it to an int to
then
only convert it back into a string. And so you might think, well, maybe we should just change
this to a string. But if you change that to a string, then it could potentially open up it
to
us to issues where the user could type in something like this, which isn't a valid ID. So we
do have to validate it as a number, convert it to an int and then convert it back to a string once
again.
Let's save this. Our error should go away. And let's try this. All right, no error. So it
looks like everything's
good. And so now it's just a matter of cleaning up our code and getting rid of all of the
other code. So I'm going to rename this. So this is just going to be post. So we can keep
everything
else the same. So if we didn't find a post, right, which means, you know, the database return
nothing, it's going to be set to none. And then we can raise the usual exception. But if we did
find
a post, we're just going to return it. Now, if I get a post of one, you can see that we've got
that
post, which is clearly from the Postgres database, because it has created that field. And then
if I try to grab a post of an if I try to grab the ID of a post that doesn't exist, try five, we can
see
that post with the ID of five was not found. And let's just double check to make sure that
there is no post with an ID of five. And there isn't a perfect. And you know, guys, one interesting
thing
later down in some of the next path operations that we work on, I did run into a weird issue
where if
I didn't have a comma right after this, even though there's nothing that comes after it led to
an issue, I'm not sure why it did that. But keep in mind, if you do run into some weird issues, just put
in
this extra comma, for some reason or another, it potentially fixes some issues. Okay, so just
keep
that in mind. I don't have an explanation for it. You know, maybe it's listed somewhere within
stack overflow, or I'm sure someone eventually or occasionally asked this question, and I
just
couldn't find it. Alright, so now let's work on deleting a post. I'm going to remove
these
comments to clear up some space, we're going to do cursor dot execute, you guys should already
know
the exact SQL statement that we need. So it's going to be select, sorry, not select, delete
from post table. And then we're going to delete by ID. So we'll say
where ID equals. And then remember, we don't want to pass in the the user input
directly,
we want to do that placeholder again. And then we also want to we want to do a returning. So
we'll
see what that post was before it was deleted. And then we'll pass in the value of the post. So
this
is going to be ID. But we want to convert it to a string so we don't hit that error
again.
And then I'm going to do that empty comma just to avoid any potential issues. And then we have
to do a cursor dot fetch one to get the deleted post.
I'm going to save this as a variable. So I'll say deleted underscore post equals
that
we can remove this. And then for this exception, when we try to grab a the ID of a post that
doesn't exist, we can just say, if deleted underscore post, which is the post that
we fetched, wasn't found, we're going to throw an error. We can delete this. And
remember,
there's one last thing, right? Anytime we want to make a change to a database, we have to
connect,
commit to it. So we'll do connection dot commit. And then everything else, we can keep our
code
exactly the same. And then I'm just gonna run this just to get so we have a post with an
idea
of one to enforce. So let's go ahead and delete one of those. So if I delete the post with
an
idea for what do we get? Oh, sorry, we're still under the get let's go to our delete. This is
our
delete. And then we're going to delete with an idea for 204 no content. So that's good. And
let's
double check our Postgres database to see if it got deleted. We can see that it's now
gone.
And then let's try to now grab the with the same one, let's try to delete the same post, which
no longer exists, we should get a 404 post with idea for does not exist. Perfect.
And last but not least, the final path operation we need to update is the update post path
operation.
We'll grab the cursor object. And we'll execute our SQL statement again. And so here, we'll do
update posts. And here we want to set the title. And what do we want to set
our title to? Well, that's going to be passed in from the users or any kind of data we get
from the user. You know, we have no idea if they put in, you know, very suspicious SQL data. So we
always
want to put the placeholders. And so all the fields that they can update are going to be
the
title, the content and published. So then the next one's going to be content. And the last
one's
going to be published. And then we'll pass in those values. And that's going to come from
our
post object. So we'll do post dot title, post dot content, and finally post dot
published.
And just like we had with inserts and delete, we can do a returning so we can actually get the
returned, we can actually return the updated object to returning stars, we can get all
the
columns. And then to actually fetch that updated post, we have to do cursor dot fetch
one.
And then we'll save that in a variable called updated post.
And then we can remove this, this is our previous code. And then for the little check to make
sure that we actually got an updated a post, we can get dated post. If we didn't, if it returned
none,
that we're going to send a 404 because a post with that ID didn't exist. And then we can
delete all of this nonsense. And then what we return back is going to be updated post up, make sure you
get
updated with a D or then if you just do update post, it tries to call it this own function,
which doesn't work. And I'm just gonna see what I have in my database. So I've got a post with
an
ID of one. So we're gonna update him, you can see currently the title is first post. So if I
go to my update post, I'm going to send it with the ID of one, we're gonna update that post, and
then
it's going to have an updated title. So if we see that get updated, then we know it worked. So
we'll send do a send, we could see what we got back. And so it looks like it updated. But let's
actually
double check with our database. And it did not update. And I'm sure you guys can take a
guess
as to exactly what went wrong. Remember, anytime you want to make changes to a
database,
we need to do the connection commit. So let's try this again. Alright, so we got the same
result,
let's take a look at our database, run this, and then we've got updated title, it looked like
it
updated both. And that looks like a bug. So what did we do wrong? Well, if you look at our
code,
we're updating every single post, because I didn't provide a where condition, what post do I
need to update? So that's a mistake on my end. So we have to do where
and then we'll say ID equals and that ID is going to come from this value. So we're going to
put another placeholder there. And then that last value is going to be
the ID converted to a string, just like we did before. So simple
mistake.
Now save that. And before I do that, I'm just gonna change this back to first
post.
I'm gonna change this back to second post. And then it's gonna have some random
text.
And then we'll save that. Then let's update this. Alright, so we updated our code. And then
let's
take a look at our database. We'll run this again. And we can see that it was updated. And
you'll
notice how that first post then moved down the list, because it was most recently updated. But
we updated just one post this time. And then last but not least, let's try to update a
post
that doesn't exist. So I'm going to search for an ID of 23. Try to update that. And then if I
do that,
we can see that we get a little bit of an error. So what happened here, we got data equals
null.
And so it looks like this didn't work. And it looks like it got it changed back to update
post. So
make sure you reference this variable. So I need that D. Now let's try that. And now we get
the
correct 404. When it comes to working with a database within a Python application, or
really
any application across any language, there's a couple of different ways of interacting with
the database. And so we saw how we can use the default Postgres driver to talk to a Postgres database
by
sending SQL commands. And this is my preferred method of working with databases, because I'm
fairly comfortable with SQL. And so it just makes sense to use raw SQL and send that directly
to
Postgres using whatever the default Postgres drivers for your specific programming
language.
However, there are other methods. And so you'll see that one of the popular ways of, you know,
really working with databases is using what's referred to as a object relational
mapper,
or an ORM. And so an ORM is a layer abstraction that sits between the database and our fast
API
application. And so we never actually talked directly to a database anymore.
Instead,
we talked to the ORM. And then the ORM will actually talk to our database. And so some
of
the benefits are is that we don't actually have to work with SQL anymore. So instead of using
raw
SQL, what we'll do is we'll actually use standard Python code, calling various functions and
methods
that alternately translate into SQL themselves. So let's see what that actually looks
like.
And so with our traditional application, which is what we have now, we've got our fast API
server, and it's going to talk to our database by sending SQL using
the default Postgres database driver. However, with an ORM, what we can do is fast API no
longer
has to talk SQL. Instead, fast API will actually use regular Python code to send, you
know,
specific commands to an ORM. And then the ORM will take that Python code, convert it
into
regular SQL, and then using the same database driver that we're using for it'll actually talk
to the database. And then it'll eventually send that result back to us because the
databases,
they can only talk SQL. However, by doing like this, we can kind of abstract away that SQL
complexity and make use of very common Python objects, and various other Python features
to
actually generate and build queries, as well as to create and define tables. So let's take a
look
at what an ORM ultimately allows us to do. So one of the first things is, instead of us going
into,
you know, PG admin, and creating the tables and all the columns ourselves, what we can do is
we can
define our tables as Python models. And so if you take a look at the code down here, what we
can do is we can actually define what our tables and Postgres are going to look like.
And so you can see here, we give it the table a name, and then we could specify each of the
columns, you'll see a lot of the common fields that we already worked with. So the
types,
so this is an integer, this is a string, we can also see if a field is nullable, we can see
that the ID is set to be a primary key. So we're using standard Python classes to define our tables
so
that we don't need to do it ourselves using PG admin. And on top of that, queries can be
made
using regular Python code. So we can actually chain on different methods to construct
various
Python to construct various SQL queries. And so if you take a look at one of the queries down
here below that I provide as an example, you can see we just do query, we provide the specific
table
that we want to query, and that's going to be based off of the specific models. And then we
can just pass in whatever filter commands, and then grab the first entry. So you can see
that
there's no SQL anymore, it's just standard Python code. And so that makes it a little bit
easier for
some people who may not be as strong on SQL. And so they don't have to worry about writing
these complex SQL commands, they can just chain on specific methods to a, to a DB query to
generate
their final SQL commands. And so the, you know, there's a couple of different ORMs, I'd say
the
most popular one within the Python world is SQL alchemy. And one of the things to keep in mind
is
that this is a ORM, that's a standalone library, right? So this has this library has nothing
to do
with fast API, we're going to use it in our fast API application. But I want you guys to
understand that it actually has no relationship with fast API, it's not part of fast API, it's its own
library.
And you can use SQL alchemy with any web development framework, you can use it with any Python
application, even if it's not a web application as well. So in the next video,
we'll start taking a look at how to work with SQL alchemy and how to set up a database
connection using SQL alchemy instead of using the default Postgres driver. Okay, so in this
lesson,
we're going to start taking a look at SQL alchemy and get that set up in our project. And so
there's a couple of things that I want you guys to take a look at. So the first thing
is,
I want you to go to the SQL page. So if you just search for SQL, you'll get to the main page.
And
then if you want to go to library, and then references, and then version one dot four, which
is the one we're going to use, however, I believe they're going to come out with a
version
2.0, which is going to be pretty different. So if you are watching this video in the future,
you definitely want to install version 1.4, so that you can follow along with this
course.
So you can click on that. And so there's a nice little tutorial you can follow. And then
there's also some reference documentation when it comes to setting up the ORM. So if
you
click on session session usage, this will show you how to set up a session. However, if you go
to the
fast API documentation as well, fast API, they've got great documentation. So if you go to
the
documentation and then select SQL relational databases, this is going to show you how to set
it up with SQL alchemy. Okay, so first things first, let's go ahead and do a pip install for
SQL
alchemy. We'll do pip install. And then SQL alchemy. Now guys, I want you to remember
that,
well, first of all, I'm running 1.4.2.3. But the thing is SQL alchemy doesn't know how to talk
to
a database, right? It has all the code for us to write Python, define all the models and
things
like that. But it doesn't actually know how to talk to a database, it actually needs a
database driver. And so if you take a look at all of the packages that we already installed by doing a
pip
freeze, right, we in the last video, or in the last section, we installed our Postgres
database
driver. So whatever database you guys plan to use, so if you want to use a MySQL database
with,
with SQL alchemy, you have to make sure that you install the underlying driver as well,
because ultimately, that's what's used to talk to the database. Since we're using Postgres,
we
need to install this as well. But since we were using it originally in the previous sections,
we are how we already have it installed. So we don't need to worry about doing that. But
keep
that in mind, right, SQL has no way to talk to a database, it still needs the underlying
driver to communicate with it. And that's usually how all orms work. But now that we have that on inside
our
app folder, I'm going to create a new file, and I'm going to call this database.py. So this is
going to handle our database connection. And from here, what I'm essentially going to do is
just
really follow this documentation. So if you take a look at it right here, we can just copy all
of this code right here when it comes to importing statements. So this is just going to import
all
of this SQL alchemy code that we want. All right, and then we have to specify a our
connection
string. So where is our Postgres database located? And so if you take a look at how we had
this set
up before using the default Postgres database driver, right, we pass it into this
connect
function. However, a lot of times when it comes to working with databases, there's a unique
URL
that you create to connect to any kind of database. And there's a specific format for that.
And the
format for that is actually pretty simple. So go to database py, and I'm going to create a
variable called SQL alchemy underscore database underscore URL. So this is going to be a regular string.
And
the structure of the URL for a Postgres database, it's going to be Postgres ql, because that's
the type of database, colon slash slash, then we have the username that you have to put in colon.
Then
we have the password. And then we do at and then this is going to be the IP
address.
I'll say IP dash address, slash host name, whichever one you're using.
In our case, that's going to be local host. And then you have to provide the database name
that you want to connect to.
So that's the format of a connection string that we have to pass into SQL
alchemy.
And then what we do is we say we have to create a engine. So the engine is what's responsible
for SQL alchemy to connect to a Postgres database. So you do engine equals,
and then we're going to reference this create engine. And then we pass in our connection
string. So it's a SQL alchemy database URL. However,
this is not filled in yet. So we're going to fill in this data. So go ahead and put in your
username. So if you haven't created a custom user is going to default to Postgres, put in your password.
So
we'll do password 123 for me, the IP address slash host name, this is going to be local
host.
And then the database name is going to be fast API or whatever you created your database
as.
So that's your connection string. And like I mentioned before, it's never good to hardcode
this value somewhere in your code, because now you've got your password stored in your
code,
and it's going to get checked into GitHub, and then anyone can see it. So this is bad
practice. However, we will change this in the future. Right. So like I said, the engine is responsible
for
establishing that connection. However, when you actually want to talk to the SQL
database,
we have to make use of a session. So we do session, local equals session
maker.
And then we just pass in a couple of values. So auto commit false, auto flush equals
false,
and then bind equals engine. And all of these values, these are just some default values.
Now
keep in mind when you're creating the when you use when you use the create engine function. If
you
are using a SQLite database, you have to pass in this option as the second parameter. So you
do
the database URL, and then you do connection args equals check same thread equals false. This
is something that's exclusive to SQLite, because it's I guess it's just running in
memory,
you don't need to do this for Postgres, you don't need to do it for any other SQL based
database.
And then you can see in the documentation, this is what the connection string will look like.
So it's just confirming what I already showed you guys.
And so we can just kind of keep scrolling down. And we don't really need anything
else,
except for this last line right here. So we here we have to define our base class. And so all
of
the models that we define to actually create our tables in Postgres, they're going to be
extending this base class. So we can just copy this and then paste it into here. And when I was
first
doing all of this, you know, it was a little confusing and overwhelming. Just think about
that.
Just for now, I want you to assume that it's more of just a cut and paste job. It's just a
copy and paste. The only thing that really matters that you're going to be changing is going to be
that
Postgres URL. But everything else for pretty much every project, you can just copy and paste
to this entire code. And now within our database.py file, that's all we have to do. The next thing that
we
want to do is define our tables. Like I said, with an ORM, we no longer have to create
tables
within pgAdmin or any CLI utility, we can define it as a Python model. So what we're going to
do
is we're going to create a new file. And this is going to store all of our models. Well, and
we'll call this models.py. So every model represents a table in our database. And what
we're actually going to do is let's take a look at our database. And let's see how we can
define this as a model within Python. So the first things first, we got to import a few
things.
And the main thing that we got to import is from our database file, we have to import base. So
let's do import database. Sorry, that should be from that database import base. And then
we
can go ahead and define our model. So right now, we've been dealing with only working with
posts.
So we're going to create a model for posts. And like I said, this is going to actually create
the table within Postgres. So what we're actually going to do is I'm going to delete this
table
eventually. And then we're going to have Python and SQL and my fast API application and SQL
alchemy
created for us on the fly. But I'm going to keep this table for now, just so we can take a
look at all the fields that we have. So here, I'm just going to call this post. And keep in mind when
it
comes to classes in Python, you always want to make sure that they're capitalized. And then
this has to extend base. So this is that base model from SQL alchemy, and we just have to extend
it.
And there's a couple things that we have to pass in. The first thing is, what do we want to
call
this table within Postgres? Because we have this class name, which only Python knows, but we
can
specify a specific name within Postgres. Now our table name right now is called posts. So I
think
we should just keep it at that. So we could say underscore underscore table name,
underscore,
underscore equals, and then the name of the table you want. So here, we're going to name it
posts. And then now we have to define all of the columns, right? So just like we went within PG admin
and
went column by column creating it specifying all of their constraints and things like that,
we're going to do this within within our Python code. So we'll say the first thing is we want
an
ID. So we'll say ID equals. And then how do we actually create a column? Well, we have to
import
something from SQL alchemy. So we'll say from SQL alchemy, import column. So now I can create
a
column. And then there's a couple of different fields that we have to pass in. So the first
thing
is what type of column is it? And so if you go back to our post table and go to properties,
right,
when we went into columns, the first thing they have to specify is what is the data type. And
so we have access to most of the ones that we see in here. And but we have to import it from
SQL
alchemy first. So the ID column is going to be an integer. And so we have to actually import
that
we'll do integer. And now I can pass an integer. And that was a mistake. Let me delete
that.
And then if we take a look at our ID field, we did say that this was a primary key. So we'll
say
primary key equals true. And then if you want to, you can also say
nullable
equals false. Alright, so that's the equivalent of doing the not null. Alright, then we
can
define the next column was going to be title. And then we call column, this is going to be a
string
now. But we have to import string from our SQL alchemy. And once again, this is going to
be
nullable, set to false. We can't leave that blank. The next column is going to be fairly
simple,
similar, we've got content. This is going to be a string and nullable is going to be set to
false.
Then we got published. This is going to be a Boolean, but we got to import that from the
library.
And this one can be left as empty. And if you remember, we set a default value for our
publish.
So it's always going to be by default set to true if we don't pass in a value. And I believe
that's how we do it. However, I'd have to double check just to make sure that's what it
is.
Now the last column that we'll need to add is the timestamp, but we're going to hold off on
doing that for now. And we're going to leave it as such. And then later, we'll come back to adding
that
timestamp in. Alright, and so we're pretty much done with our model. And what we want to do
is
in our main file, we want to copy this command right here. So this create engine. So this
is
going to create all of our models. So we'll go to our code, we're going to go to my main
file,
and somewhere up top, I'm going to create that right there. However, we have to import the
right
commands. And so the first thing that we want to import is our model. So we'll
do
from star import models. And then from our database file, we'll say from dot which is
current
directory. So we're going to grab a file from our current directory, we're going to
import
right from our database file, we're going to import engine.
And so at that point, that is good to go.
And then finally, if we go back to our documentation, the last thing that we have to do is we
have to create this dependency. So just go ahead and copy this for now.
And just paste it in here. And we want to import session local as well. And we should no
longer
get any errors here. And so what this ultimately is going to do is the session object is kind
of
what's responsible for talking with the databases. And so we created this function where we
actually get a connection to our database, or get a session to our database. And so every
time
we get a request, we're going to get a session, we're going to, you know, be able to send SQL
statements to it. And then after that request is done, we'll then close it out. And so it's as
much
more efficient doing it like this, by having one little function, and we could just keep
calling this function function every time we get a request to any of our API endpoints. And so now that
we've
got our dependency to actually get that session, for all of our path operations, where we want
to
perform some sort of operation on the database, what we're going to do is with inside the path
operation function, we're going to pass in another parameter, and it's going to be session
equals
depends, and then we're going to call this get DB function. So what that's ultimately going to
do is this line right here, don't worry too much about it. But like I said, it's going to create
that
session to our database, that we can perform some operations and then close it once that
request is done. And then we can repeat that process for every single one of our path operations. Or
anytime
you send a request to any API to endpoint, you're going to have this being passed into the
path
operation function so that we can create that connection and then close it out. And like I
said, a lot of this is just copy and paste. So once you write this down, even if you don't really
understand
it, you don't really have to touch it ever again after that. Alright, so let's copy
this.
And what we're going to do is I'm going to create another route, or another path
operation,
just for testing purposes, because I don't want to mess up any of our other code. And so I'll
just say at app dot get. And this is going to be at slash
SQL alchemy. So this is just a test to see if it works. And I'll say def test
posts.
And then we want to pass in that that code that we just copied right here.
However,
we don't have a session or depends in this case. So we're going to have to import those. So
from
our database file, our session is going to be from there. Actually, sorry, that's not
actually
coming from there. Is it Do we have a session in here? We actually do not have session here.
So
instead, this is not going to come from there. Actually, instead, we're going to import that
from SQL alchemy. As a import SQL alchemy dot or m import session. And I realized this should be
a
from and then we also have to import the depends method from our fast API library. And so now
if
we go down to our route, we should get no more errors and it looks good. And then here, we're
just going to return status success for now. And then finally, the last thing that we're going
to
do is we're going to delete our post table, I'm going to do a delete up, yes. And we have
no
more tables. Now, I can close this out. I didn't mean to save that close that don't save
it.
Right now we've got nothing. So let's save everything. And pray to God that there's
no
errors in it. So it doesn't look like there's any errors. And just by saving that code
and
restarting our application, what should have happened is, once our code ran, and we run this,
I believe this should actually create the tables within Postgres. So let's refresh this real
quick,
I'm going to just right click here, hit refresh. We go under tables. Look at that, guys, we
now
have our post table, even though we deleted it. So if we take a look at our post table and go
under properties, columns, you could see it's got the three columns that we default for columns that
we
defined. And that all comes from our models.py. And so we've got the four columns that we
defined
here. And so if whenever we start our application, SQL alchemy, we'll check to see if there's
a table
called posts. If it's there, it's not going to do anything. If it's not there, it's going to
go ahead and create a first based off of what we defined in the model. Now, there's one last
thing
that I want to do just to kind of clean things up. In our main.py file, I don't like having
this
get DB function within here, I want to keep all of my database code, all of my database
initialization
code within my database.py file. So what I'm going to do is I'm going to actually cut this
out,
paste it into my database.py file. And then we're going to import this into my main file so
that I
don't have to have that in my main file, cluttering it up because our main file is already
fairly
large. So for my database file, I can import get underscore DB. And now we no longer
need
session local because it's showing us it's grayed out so we can remove that. And at that
point,
everything should still be working. So just as just to double check, I'm going to delete this
once
again. And then I'm going to save this, which is going to trigger a reload of our
application.
And then once our application reloads, if I refresh my database, it should have created
another
another post table. And it looks like it did. And let's just double check that all of the
columns are there. And it looks like they are. So everything looks good. In the next
video,
the one thing that we do need to change is that we have to add the created at column. So we'll
tackle that in the next video. In the last video, we were able to get SQL alchemy to generate
our
own post table. However, if we actually poke around with it, we'll see that there's a couple
of issues. So if I go to right click on it and select properties, and I go to columns,
all right, we can see that all of these fields are set to not null. And the primary key is
set
to the ID field. So everything looks good so far. However, when I go to the
published
column, and I go to constraints, I can see that the default isn't set. And so if we go back
to
our code, I actually made one little mistake, right, this default is not going to give us
what
we need. Instead, what we need to do is we need to set this to server default, because
it's
ultimately the the Postgres server or the database server that's going to actually send the
default.
So I'm gonna put it in quotations. As a string, we can just set this to be
true.
And then if you haven't added this, go ahead and add nullable to false, I'm going to make all
of my columns are not allowed to be empty. That's because the server is going to add the
default
to true for this. So let's try this out. Now, if I hit save right now, and I go to my
Postgres
database, and then we can just right click on this hit refresh. And then we do properties
again,
and I go to column. And we go to sorry, go to the published column, we'll see that there's
no
constraints. And so this is kind of a limitation of SQL alchemy, because SQL alchemy will
generate
your tables. But it does it in a very simple manner. What it actually does is if I go to my
main.py file, and then it runs this code, I believe this is the code that actually creates the
tables,
what it does is it'll go through all of our models, and it will look for a table named called
posts. If it doesn't exist, it will then create one based off of these rules. However, if the
table
already exists, even though we've changed the different attributes, the columns and things
like that, if it looks for the if it finds a table with that name already in there, it's
not
going to touch it. So it's not going to help us modify tables and things like that. In fact,
if you actually take a look at the documentation, I've got so many tabs open, let's see if I
can
find it right here. I search for alembic. And so normally, when it comes to creating your
tables,
when it comes to handling migrations, which is a term that's used for changing the
columns,
and the schema of your tables, then you want to use another software called alembic. SQL
alchemy
isn't really meant for handling database migrations, we're making changes to it. So that's why
we don't see it automatically make those changes. So for now, what we're going to do to get around
this
limitation is, I'm just going to cancel out of this, and we're just going to delete the table.
So we'll drop the table. And then we'll go back to our code. And I'm just gonna hit
save.
And so now that it runs, again, it's going to look for a table called post, since we deleted
it, it's not going to see it, it's going to then go ahead and create our table.
And now if I just right click on tables, hit refresh, we should see that we have a post table.
And if I go to properties, we can then go to columns, published. And if we go to
constraints,
we can see the default set to true. So it looks like that fixed that issue. Now, the second
thing I want to do is I want to add our timestamp column, because that's very important, we
need
to know when a post was created. So let's create a field called created, created underscore
at
column. Now, what's the column type going to be? Well, we're going to use a column type of
timestamp. So type in timestamp, we can let vs code automatically import it. But all it's
going
to do is it's going to import it from SQL, SQL, alchemy dot SQL dot SQL types. And then
within
here, we have to pass in a property of timezone equals true. So this is going to also add in
the
timezone. And that's going to be a capital T. And the next thing we want to do is provide a
it's
going to be nullable set to false. So it can't be left empty, but we're going to give it a
default
value if the user doesn't provide it, which they never will. And so we'll do server default.
But
we have to do something a little bit different here, I have to import a function or method
called
text from SQL alchemy. So from this, right here, SQL alchemy dot SQL dot
expressions,
and the text. And then here, we're going to do now, right, just like we typed in
within
our Postgres database, you know, if we wanted to manually do this, we would just go
under
the column, let's say we had to create a column, we can just go in there, go into constraints,
and then type in now like that. And that's going to generate a timestamp. So we're doing the
same
thing, but we're just doing it through Python code now. So once again, we'll save
this,
we're going to drop this table so that we recreate it.
And then we'll save it one more time, this is going to trigger a reload and then a refresh of
the table, we should now see it. So let's go into properties. Let's take a look at our
columns,
we've got the created at column. And then if I go under constraints, we can see the default is
now set to the current time. So we've got that working, let's just quickly test this out. So I'm going
to
go into view, edit all rows. And I'm just going to create a post. We'll save this and let's
make
sure all the default values gets it looks like that's good. And it looks like the time is set
just right as well. So now that we have verified that SQL alchemy has created the proper
tables
within Postgres, it's time to go ahead and create our first query. And like I mentioned in a
previous
lesson, with SQL alchemy, or really any ORM, the way we do queries is going to little bit is
going to be a little bit different, we're no longer going to rely on regular SQL, instead, we're going
to
use basic Python methods. And so go to your main folder, sorry, your main file, right. And
we
saw this test route that we defined. So what we're going to do is we're going to run our first
query
within this test route. And then afterwards, we can go ahead and delete it. And then we can
update all of our pre existing path operations. And so like I mentioned before, anytime you want
to
perform any kind of database operation with SQL alchemy, within fast API, we want to make
sure
that we pass it as a parameter into our path operation function. So you just call DB. So
we're
saving it inside a variable called DB. And then we're just calling the session object. And
then we're calling the get DB function within depends. So this makes it a dependency. And so if
you
already forgot what that is, just go to your database, you can see that it's this function
right here. So this is going to actually create a session towards our database, for every
request
to that specific API endpoint, and then it's going to close it out once we're
done.
And then within main, there's one thing that we have to import, and that is models. So you
just do from dot import models, that's going to import this specific model so that we can actually
make
queries to it. All right, and to make a query, right, we're going to access that database
object,
which is getting passed into our function. And then here, we want to tap into the query
method.
And then we have to pass in the specific model for the table we want to query, we only have
one
model in this case, which is our post model. So that when we use this specific model, it's
going to allow us to make a query to our post table. And here, so we'll call models dot post. So
that's
going to allow us to access that model. And then from here, what we can do is, we have a
couple of different methods that we run can run. But since we want to query all of our posts, we just do
all
is this is going to grab every single entry within the post table. And then here, we could
just save
this as a variable called posts. And then for the return statement, I'm just going to say
we're going
to return data, and then posts. So let's test this out now. And I'm going to go to get
posts.
Actually, I'm just going to create a new test, test routes are a new request. I'm just going
to copy the URL. And then this is stored in SQL alchemy. Let's try this out. And
this should result in us getting just one post. So let me just double check my Postgres
database,
just to make sure that we only have one post. So I'm just going to open up the query tool. And
we'll just do select star from posts. Yep, and it looks like I only have one post. And so
that's
why we only got one post in our request. And if I add a new post, and save that, and we
make
a new request, we should get two posts. So that verifies that we are successfully connecting
to our database. And we are successfully retrieving those posts. And so I want you to stop and
really
take a look at how the queries vary when it comes to working with an ORM versus working with
regular
SQL. Because if we actually see the path operation for getting posts at slash posts here, this
is
where we're using regular SQL. So here we do select star from posts. But if you take a look at
this, there's no SQL here, right, we're just tapping into the database object. And we're going to call
a
query. And then from here, we have to tell what specific model, which in this case, remember,
these models represent tables. And then we just say I want all posts. Now, what's even
more
interesting is if I remove this, and then for now, I'm just going to hard code, you know,
successful,
just some random data, I'm going to do a print posts. So right here, we're calling the
database
object, and then we're calling query. So what actually happens when we just do this? Well,
let's save that, we're going to send a request, we're not going to get any data back,
that's to be expected. But take a look at this, right, we're actually taking a look at what
the query object returns. And you can see that it's just returning a regular SQL statement. So
it's
saying I want you to return post that ID, and then it's just renaming it posts underscore ID,
then I want to grab post that title, we're renaming it as post that title. So it's grabbing all
of
the columns. And then we're going to grab it from our post table. So it's really no different
than
just running, you know, select star from posts, essentially, that's all it's doing. But it's
just
renaming all the columns to be a little bit more easy to read. So you can see that these
queries, this query object is just performing SQL. So it abstracts all of the SQL away from
you.
And it handles all the logic of generating the SQL so that you don't have to know, or you
don't have to have as solid of an understanding of SQL. And this allows you
to really focus on building out the API, this allows you to focus on working with Python
and
less with SQL in the databases themselves. So now we know when we call query, this actually
creates
the query, however, to run the query, we have to run a specific method. So there's a couple of
methods that we can run. But if you want to grab everything, then you just call all and then
it's
going to take this SQL query, and then it's actually going to run it against the database and
then return it. But until we call this last method, it's nothing more than just a SQL query that
hasn't
been run yet. Alright, so now that we know how to fetch all posts, let's just go down
here.
Right. And let's just put in this logic. So this is our actual endpoint for retrieving
posts.
This is our get posts. And what we're going to do is I'm going to comment this out, just so
that,
you know, later down the road, if you guys want to just take a look back how you do it with
regular raw SQL, we still have that within the code base. And anytime you want to work with the
database,
remember, you have to pass it in to the path operation function. So this is going to make it a
dependency. And you'll see by doing it like this, it'll make testing a lot easier.
And then we can just copy this right here. And then we're going to return the post. So
let's
test that out. And now if I go to get posts, hit send, you can see that we successfully
retrieved
both of our posts. Alright, and it really is as simple as that. And then in the next
video,
we'll start taking a look at how we can create posts and so on. In this video, we're going to
start tackling how to create a post using a ORM or specifically SQL alchemy. So once
again,
I'm going to copy out or comment out all of the SQL code. And what we're going to do is we're
going to see how we can actually create a brand new object, or a brand new post in this
case.
So first things first, anytime any of your path operations need to work with the database, we
need to make sure that we copy this exact input argument into the path operation
function,
this is going to give us a access to our database object so that we can actually make queries
and make changes to our database. And by passing it in like this, it's going to make it a
dependency
and it's going to make it a lot easier for testing and, and things like that. Technically, you
don't have to do it like this. You could just import it directly in there. However,
by doing it like this, like I said, testing becomes a little bit easier. All right, so we've
got our models, right, and we have one specific model. So this model represents our
posts.
And so to create a brand new post, we have to reference this model and pass in the
specific
fields that we want to create a new entry with. So first of all, we have to make sure we
import that model. And you can see that we are importing all of our models by doing from dot import
models,
so we can access models dot post in this case to x that specific model. And so here, I'm going
to
do models dot post. And then we have to pass in the properties of the Brandy of the brand
new
post that we want to create. And so what are the properties, right? If you take a look at our
SQL statement, we need the title, the content and the published Boolean, right? And that's all
going
to be derived from the request that we get, which is going to be stored in the post object. So
I can reference all of those fields. So I can say the title for the brand new post is going to be
set
to whatever the title the user sent that and that can be accessed with post dot title. The
content
of the brand new created post is going to be a content equals post dot content. And then
the
same thing is going to be for published. So it's going to be post dot published. And if you
take a
look at this code so far, this doesn't look too different from the SQL statement, right? This
says insert into posts. And then we pass in post that title into the title post that content into
the
content, and then post that published into published, right. And so this pretty much exactly
the same thing. We're not doing anything different. We're just having SQL alchemy handle all
that
logic. So we can do everything in just standard Python code. And there's no SQL anymore. All
right, and the next thing that we want to do is well, let's save the result as new
post.
And let's see what happens. So I'm going to save this. And then we're going to set a little
query.
So first of all, I'm going to create a new post and I call this a welcome to fun
land.
So much fun. Okay, and so then we'll, we will send a request. And we got some data back. And
so it
looks like it worked. However, take a look at our, our data, we've got a title, we've got a
content,
and then we've got published. But where's the ID? Where's the created that field, all the
fields that the database creates. So this makes me suspect that this didn't actually work. So
let's
go to Postgres. And then go ahead and make a query to your table. So select star from
post,
that's going to grab all of them. And you'll see that based off of the title, welcome to fun
land,
I do not have that in there. So it looks like this was not successfully able to create a post
within our database. So let's take a look and see what went wrong. And if you actually take a
look
at the the previous method, right, you could see that we actually have to commit something to
the
database. So we can create an entry, but it doesn't actually get pushed to the database until
we do a
commit. And so I'm suspecting that we probably have to do a similar thing. So when it comes to
SQL alchemy, what we need to do is, first of all, we have to tap into the database object.
Now,
we want to say DB dot add, and then we're going to add this newly created post. So this is
going to
add it to the database. However, just like with the connection commit, we have to commit those
changes. So we do DB dot commit. And then the last thing that we need to do is, if you take a look
at
our SQL statement, we have this returning star. So that's going to return back the newly
created
post. With SQL alchemy, it's a little bit different, right, because we don't really have
access to that
underlying SQL code, we can't add a returning statement to this. So the way that you do this
in SQL alchemy is you do DB dot refresh. And then you pass in the new post object. So what this
is
going to do is we create a brand new post, we add it to our database, then we commit it. And
then what we're gonna do is we're going to essentially retrieve that new post that we just created
and
store it back into the variable new underscore post. And so all of this code should be
identical
to this code. And so now if we will just return back new post, and I think that should fix
our
issue. So now if I hit send, we do see an ID, and we do see it created, I feel so it looks
like it
worked, but we want to double check within Postgres. So if I hit run, and we can see welcome
to funland,
so it looks like it's now successfully working. And before we wrap up this video, there's
one
thing I don't like about the way we've done things, right, and this isn't necessarily going to
break or cause any issues. However, you're gonna see it's very inefficient, depending on what
your
models look like. Now our model has only a couple of fields, and the user only has to really
pass in
two to three fields, which is title content and published. So it's not that big of a deal. But
imagine if we have a model with 50 fields, there's no limitation, right? You could put as many
fields
as you want. Well, then things are gonna get a little bit more difficult. Because if you see,
we have to extract all the fields from our schema, right, and then we have to kind of pass it in.
So
we have to say, oh, title equals post title, content equals post content, and then published
equals post published. Now, if we have 50 fields, we're gonna have to do that 50 times. And
that's
a little inefficient. And there's actually a much easier way to do it. And the way to do this
is,
we have access to the post object, which is, you know, this is a pedantic model. So it's going
to
ensure that it matches the schema that we set. We want to say post dot dict. Now we know
that's
going to convert it to a regular dictionary. So if I do a print, save that, and then just send
a
request, right, take a look at this, we've got a regular Python dictionary. However, we need
to be able to automatically take that dictionary and convert it to this format, where, instead of
having,
you know, title like this, and then welcome to fun land, it's going to be title equals, and
then post our title content equals post content. So how do we do that? Easy, all we
have
to do is unpack the dictionary. So you just do a star star, and it's going to put it into this
exact same format. So we can remove all of this and just say star star post dot dict. And
that
should ultimately do the same exact thing. Except now if we go back to our database, or our
model
and add an extra fields is going to automatically unpack all of those fields for us. So we
don't have to manually type it out. All right, we'll save that I'm going to just put in some
extra
text just to make sure we can verify that. And I realized we got to remove this print
statement,
it's gonna throw an error. And we'll hit send, it looks like everything worked. Let's go to
our
database, search and it looks like it worked. So you can see that if we go back to our
code,
a lot cleaner than having to type out each one of those fields. And we can put this all in one
line.
So it looks a little better. Alright, so now it's time to handle querying for an individual
post.
So I post by a specific ID. So once again, we will comment out the SQL
code.
And we will do this using SQL alchemy. So once again, copy the database dependency right here,
and then just pass it in. And this should actually be an integer. I don't
know who changed that I may have left that in from the previous video. Yeah, it should be an
integer.
Okay. And now what we want to do is, just like we saw when it comes to querying all
posts,
we have to do DB dot query, pass in the specific model that we're interested in. And then
we're
not going to do a dot all because we don't want all the posts. So we're gonna have to take a
look at a different query operation. So I'll say DB dot query, models dot post. And then we want
to
filter so we want to pass in a filter. So this is the equivalent of doing like a where you can
see that we filter by doing where ID equals and then we pass in the ID. In this case, we're going
to
filter. And then here we say, whatever the models dot post dot ID is, so this is going to
look
through all of the posts in our database. And it's going to take a look at all other IDs. And
we want to see whenever that is equal to the ID that the user requested. So the ID from this query
parameter,
or this path parameter. So we say when they're equal, that's the one we want to
return.
And if I do a print of first of all, let's save this to a variable. So I'll say post equals.
And
I do a print of post. Save that, I'll open up the terminal. And then we'll send a request for
an
individual post. We'll hit send. We get a whole bunch of errors. That's okay. Let me see.
And
that's fully expected. Alright, and then we can see the exact SQL statement that this query
actually
made. And so here we're doing DB query and then filter and we can see that we got the
select
statement. And then here it's just grabbing all of the all of the columns. Right, but then you
can
see from posts, and then it says where post that ID equals ID underscore one. And so this
pretty
much looks exactly like we wanted, right? If you look at our SQL statement,
originally,
it doesn't look any different. And so at this point, I think this should be good to go.
However,
right now, it's still just raw SQL, right, we could see what the SQL statement is. But if you
recall from the previous one, we had to do a dot all to actually send the query.
So for us, we can do a dot all technically. And this would work, right, it would grab all the
posts
who have an ID of whatever ID we passed in. However, there's one little issue with doing
it
like this. And that is that once it finds one post, it's going to keep looking through all of
the posts to see if there's any other ones. But we know that only one post can have this
specific
ID. So it's a waste of Postgres resources to continue looking when we know there should only
be one. So anytime you know, there should only be one instead of doing a dot all is better to
do
a dot first. So it's going to find the first instance and going to return that. So it's going
to save us some little time, it's going to be a little bit more efficient in that case.
All
right, let's save this. And let's try this again. And let's see if this works. All right, it
looks
like it worked, we got a post back, no errors. So I think everything is good to go. Let's go
back
to our Postgres database, we do see that there's one with an ID of four. So it seems to work.
But let's test this out with a random ID of like 666. And we should get the 404. So looks like
that's
all we need to do when it comes to fetching an individual post. And I'll remove this print
statement, we don't need that anymore. And then we can just conclude this video. Alright, so
let's
move on to handling our delete path operation. So I will comment out the SQL code once
again.
And then we're going to make sure that we pass in the database dependency into our path
operation
function. And so for our delete operation, what we're going to do is I'm going to do a
query.
And this query is going to look exactly like this query. So I can actually copy this one in
this case. So all we're doing is we're grabbing the posts model. And then we're going to filter
based
off IDs, we're going to look for the ID of the post that we want to delete. And I'm actually
going to remove this first. So I'm going to save the query by itself. And we'll save this as a
post
underscore query. I'll just save it as post. Okay, we'll understand that that's a query and
not an
actual post. And then our if statement to check if there actually is a post with that, what
we're
going to do is we're going to say if post dot, how do we actually run this query? Well, we
do
first, so that's going to query. And so if this returns back nothing, then we're going to
raise a 404. So really, we haven't done anything different from what we did here.
However,
if it does exist for the outside of this if statement, I'll do a post dot delete. And
then
we'll just say, synchronize session equals false, I believe that's the default config.
Anyways,
I don't want to spend too much time going over this. It doesn't really matter too much. But if
you want to read about it, just take a look at the documentation. And if you go under, you
know,
session basics, you could see them kind of describe all of them. But this option is
the
most efficient and reliable. So I just went ahead with that one. And it seems to work just
fine.
So this is going to delete the post. But remember, to actually make changes, we have to do a
DB dot commit. So that'll delete it. And then everything else should and can remain the same. I'm going
to
go back to my Postgres database, we're going to delete a post with the ID of six. So we'll go
to
our postman, we'll go to our delete operation. And then we'll do an ID of six. All
right,
seems to work. But let's double check with Postgres. Right now we can see that there's no
longer a post with an ID of six. And then let's test this out with a random post
number
that doesn't exist, we should get a 404. Perfect. Okay, so it's time to tackle our final
path
operation, which is for updating posts. This is one is going to be fairly similar to, you
know,
deleting a post, or even getting a post by a single ID. So first of all, comment out
the
previous Postgres code. And because we're going to be interacting with the
database,
let's make sure we copy the dependency in this case. And then what we're going to do is
we're
going to do the same thing, we're going to say DB dot query. I'm going to query models dot
post.
And then we're going to filter on the same thing, we'll say filter
dot ID. And if that equals to the ID that we're passing here, then we're going to find the
specific post we're interested in. So I'll say,
I'm going to save this as post underscore query. So right now, we're not actually running the
query, we're just saving the query because I haven't run dot first or dot all. And we'll
say
the actual post equals post underscore query dot first. So this is going to grab the post if
it
exists, if it doesn't exist. So say if post does not equal none, or if post equals equals
none,
then we're going to send a 404. However, if it does exist, then we'll grab the query
again,
say post query dot update. So we can chain an update method. So we're basically
taking
this query right here. And then we're chaining the update method, which would be the
equivalent of doing an update right here. So say dot update. And then what we want to do is we want to pass
in the
the fields that we want to update as a dictionary. So in this case, what I'm going to do is
I'm going
to hard code the values and just to show you what that looks like. So we'll say, title is
going to
be set to, hey, this is my updated title. And then the next one's going to be the
content,
we can update that to be whatever we want. And I'll say, this is my updated
content.
Then we're going to do that same synchronize session equals false as the second property
into
the updated method. Go to synchronize session equals false. And that should handle the
update,
and then we would just want to do a DB dot commit. And right now this updated post doesn't
exist
anymore. So we're just going to return successful or something doesn't matter. We're going to
change this a little bit later on. Alright, so let's go to our update, where is our update
post,
it doesn't really matter what data we pass in, because we're hard coding the values. And so
I'm going to update a the post with an ID of one if it still exists, and it looks like
it
does. So you can see right now the title is new post. So we'll update this, then the
successful
and then let's go to back to Postgres, do a query. And we can see that, hey, look, it got
updated,
hey, this is my updated title. And the content, this is my updated content. So that's all we
have to do, we just have to pass in a Python dictionary with the updated fields. And so instead of
passing
a Python dictionary, right, we can pass in our post schema, right? So that's going to have
all
of the updated fields. And we can just say, post dot dick, that's going to return a Python
dictionary.
So that should be exactly what we're looking for. And then finally, when it comes to returning
data, what we're going to do is we're going to run another query. So we're going to say, post
underscore
query. So we're going to grab this exact query object, right here. And then we're just going
to get the updated post. So we just grabbed the first one with that specific ID. And then it
should
return that though, hopefully you guys understand. And I'm just going to quickly recap exactly
what we did, we set up a query to find the post with a specific ID, then we're going to actually
grab
that specific post. So we run that query by grabbing dot first, if it doesn't exist, we're
going to run a 404. But if it does exist, then we can chain the update method to the same
query
object so that we can update it. And we pass in the fields that we want to update, we'll
commit it to our database, that after all of that's done, we want to make sure we get the updated post
and
send it back to the user. So we do post underscore query dot first. Let's test this out. And
I'm
going to put in values because these values now matter. And I'll say, my name is
Sanjeev.
Python is fun. So let's try this. What happened here? Okay, I forgot to
save.
It send, we get an error. And I realized we have a little bit of an issue. So this, the schema
that
we get where we extract all of the body fields is named post. But then we also save this as
post as
well. And so this leads to issues, because they're both named the same thing. And so I'm
trying to
I'm trying to convert this to a dictionary, which only works on the pedantic model, it does
not work on our SQL alchemy model. So we're gonna have to rename these.
I'm going to rename this updated underscore post. And then here, this is going to be
updated
underscore post. Now let's try that again. Seems to work. Let's check our database,
though.
And we can see that it was successfully updated. And then let's try to update something that
doesn't exist. And we get a 404 perfect. Before we proceed any further, I do want to provide
some
clarifications on what are the difference between a schema or pedantic model, and our ORM or
SQL
alchemy model. And I think it's important that you guys understand that. And I do realize that
there could be potentially some confusion as to what's the difference between them?
Ultimately,
what are they trying to accomplish? And why do we have both of them? So I've created a couple
of slides. And hopefully that helps sort things out. Hopefully, and hopefully it provides a little
bit
of clarification, if you guys were confused at that. But just to look at our code, you'll
notice that in our main.py file, we have one class called post here. And this is extending base
model,
which actually comes from the pedantic library. And you can see we're importing pedantic right
here. So this is what's referred to as our schema. And we also have our and I don't even want
to
explain it yet. Hopefully, you guys already know what it is. But I have a couple of slides
that hopefully make it a little bit more clear. But just to kind of show you where this is
being
referenced, it's being referenced in our path operations. So if you go to, you know, our
create posts, which wherever that is, right here, you can see that we're passing that in right
here,
and then saving into a variable called post. So we're referencing it right here. This is all
about defining the shape of a request. And so that's the pedantic model. The other model is the one
that
we have in models.py, which is the SQL alchemy model. So this SQL alchemy model defines
what
our database, our specific table looks like. But let me pull up the slides just to show you
guys,
you know, with some nice diagrams. So hopefully you guys get a better idea of what that is.
Alright, so let's start off by going over the schema models, or the pedantic model. So
the
schema or the pedantic model defines the structure of a request and a response. And so that
way,
you know, when it comes to creating a post, we can define exactly what the request should look
like. So when we create a brand new post, we need the user to provide what's the title of the
post,
what's the content of the post. And optionally, they can also provide if it's published or
not, but we've provided the default value, so they don't actually have to send that. So what we
do
is we take the request, and then we pass it through the pedantic model, the pedantic model
will perform a little bit of validation to make sure that all the fields that we need to
actually
create a brand new post are there, and that they are of the proper type. So if it's a
title,
it shouldn't be an integer. And if it's the published property, it shouldn't be a string or
anything. It's just a simple Boolean. So that's what the pedantic model or the schema
model
does. It's just there to provide some validation to ensure that the body, all the data
fields
provided in the body of a request, match up to what we want. And that's important because we
don't ever want to give the clients, which is the web browser or the mobile phones, or
anything
like that, freedom to do whatever they want, we want to tell them exactly what we need for
each specific path or route. And that's for the request. However, we can also do this for the response.
And
we haven't actually done that yet. But we can actually define exactly what that response
should look like. So you know, as our as our models get more complex, as our database gets more
complex,
there's going to be a lot of fields when it comes to your posts and your users, that you may
not want to send back to the client when we send a response. So we can actually define a
model
to dictate fast API exactly the data fields that we should be sending back. And so that's what
your
schema and your pedantic models do, they just ensure that the request and response are shaped
in a specific way. Now, the other model is the SQL alchemy model. So this is the one that
we've
been working on with the past couple of lectures. And these are responsible for defining the
columns of our posts table within Postgres. Alright, so it's going to define all the different
attributes
within a specific table. And then we use that post model to perform queries to our
database,
we use it to create, delete and update entries within our database. But this model is
fundamentally
different than the pedantic model. So that's why I created this video, I just wanted to make
sure that you guys understood what were the differences between those and understand why we need
them
both. Technically, we don't need the pedantic models. But you know, when it comes to
building
out API's, you want to be as strict as possible when it comes to what kind of data can we
receive and send to the user. And so pedantic just ensures that you know, everything just matches up
with
what we expect. But in the last video, I described the difference between a pedantic model and
a ORM
model. And there's a couple of things I actually want to do with our pedantic models. We've
just got one right here where we define what a request what a post should look like. And we use
that
in a couple of places. So if we go down to create posts, we can see we use it there. So it's
going to define the structure of the data that we received from the front end when we want
to
create a post. And I think we also provide it for maybe updating a post. Yep, we also use it
for
updating a post as well. So what I'm going to do is I want to create the schemas or I want to
move
the schema in this case, remember, this is what's referred to as a schema, I want to move that
to its own file just so that we don't clutter up our main.py file. So I'm going to go under
app,
I'm going to do a new file. And I'm going to call this schemas.py. And then within
schemas,
if I go to my main.py file, I'm just going to cut this out. And we can paste it in here. And
there's
going to be a couple of things that we have to import. So if we go to our main.py, we
definitely need pedantic, that's really all we need, actually. So if I go there, and then paste that into
there,
we should have no issues. And then, you know, this is going to actually break our application
because we need to actually import this specific schema. So if I go into main.py,
we can see here, I'm doing from dot import models. So it's going to import everything from the
models file. But we can also then import schemas as well. And so now to actually access this specific
class
post, we just have to go in our main.py file, and the reference schemas.post. And so anywhere
we use
post, which should be right here, we can do schemas dot post. And then in our update
post
as well, we can do the same thing, there's going to be schemas that post because now we're
importing it from another file. And what I'm actually going to do is we're going to restructure this a
little
bit. So what I'm going to do is remember, these are regular Python classes. And so we get all
of the
abilities that we have when it comes to inheritance, when you come when it comes to working
with these classes. So what I'm going to do is, we could define a couple things, right, we could have one
for,
we could create a class for create post, actually, that should be
capitalized,
or create post. And then that's going to extend base model. And then this is going to have
all
of the fields that we need from the user when it comes to creating a post. And so that's what
this would be in this case. And then remember, we also need to handle updating posts. And so
we
could create another model for that. On this case, I could call it update post. And once
again,
that's going to extend base model. And when it comes to the the fields that we use for an
update
post, it would be the same exact thing, right? Whoops, that should be right here. And I
don't
know why it's doing that. And in this case, there wouldn't be a default value for
published.
Because we want them to explicitly provide each column. And so at this point, right, we could
then
have two different classes for each specific request. And that's a perfectly valid use case,
because when it comes to creating, they should provide a certain amount of fields. And
then
updating, it might be completely different, right? Because, you know, let's say in our
application, we wanted to make it so that the user can't ever update a post, they can only update one
property.
And that's the publish, so they can just change whether it's published or not, we could remove
this. And this is going to make it so that the user isn't allowed to provide any other
fields,
they can only pass the published field. So that's why we would want to create different models
for each of the different requests. But what I like to do is instead of having one for
create post one for update post, and then one for the, you know, we have to eventually create
models for the response, what we're going to do is we're actually going to delete all of this. And
we're
going to create a class called post base. So I'll do class, post base, and then base model.
And so
this is a regular class, we can just copy all of these fields. And then we can extend that
class.
So I can take this post base class, and I can extend and say, class, post create, I want to
extend
post base, we can say, post base. And so it's going to by default automatically inherit all
of
these fields. And so we can just say, for creating, we can just say pass, which means it's
just going
to accept whatever post bases. So post create is essentially the same thing as post
base.
But then we can create a brand new one called class, I don't know, post
update.
And that's going to extend post base. And we can say that for updating, we can pass in
whatever
specific fields we want to add in as well. But it gives us flexibility because we can make use
of inheritance. So for now, I'm going to remove post update, and we're just going to keep it as
post
create. Because, you know, updating and creating is going to be fundamentally the same. And
that
way, we can just kind of piggyback off of this post base model, and then just create brand new
models based off of this. So what I'm going to do is going into my main.py file. And then
for
schemas, actually, let me save this. Then my main.py file, there's no longer a post, it's
going to be
post, actually, it's going to be create post create, I'm gonna do the same thing for update as
well.
That was the update, actually. So then the other one is for create post. And then this is
going to be schemas dot post create. Alright, and then the last thing that I want to do in this video is
we're
going to just delete this test route that we had defined when we were learning about SQL
alchemy. We don't really need that anymore. And then in the next video, what we're going to do is we're
going
to tackle sending a response back. Right. And as I mentioned within the slide deck, right,
just like
we can define what a request should look like, we can also define exactly what a response
should look like. So we're going to do the same thing, we're going to define a pydantic model for
a
response for what a post should look like when we send it back to the user, so that it sticks
to a
very specific schema, and then we don't end up sending data that we don't actually need to
send to the user. In this lecture, we'll take a look at how we can define a pydantic or schema
model
to define the exact shape of a response. Because right now, if you actually see any of our
path
operations, what we do is we just return a post or multiple posts or an updated post. And
whatever
data we get back from the post, we just send it back to the user. And there may be times where
maybe you don't want all of the properties or all of the attributes and columns from our post
table
to get sent back to the user, because, you know, potentially, that could be sending him
back
information he shouldn't know about, especially when it comes to, you know, like your your
user account, right, when a user logs in, we don't want to send him back his password, he already
knows
his password, we don't want to continuously transmit information like that. So there can be
times where you do want to remove certain fields or properties. And before we actually get
to
performing that, we're going to do a little bit of cleanup. Because if we go to really any one
of our requests, like if I go to get posts, and hit send, you see that we have, you know, within data,
we
then have the list or array of all of our posts. And if we look at create posts, right, you
can see
we always have data and then the actual post, I'm going to get rid of the data field just
because I
think it's unnecessary, and it kind of clutters things. So what we'll do is within all of our
path operations, we're going to just remove data. And instead of returning, you know, a
dictionary,
we're just going to return posts. And so it so fast API will automatically be able to
serialize that
and convert that into JSON. And I'm going to do that for all of these, we're just going to
remove
that data keyword for all of them. We don't need to do it for delete, but we do need to do it
for
update. And so now, if we actually take a look at what this is going to look like, I hit
send,
you can see we just get back the post and we don't get that data keyword. Same thing for get
posts, what does that look like? Perfect. So we don't have the data keyword. Now, let's
actually
define what the response should look like. And we do that with our schemas, we do that with
our
pydantic models, just like we have a pydantic model for creating posts, which happens to
just
inherit from post base. So post create is just saying that we expect the title, we expect the
content published as optional. And if you don't provide one, it's true. And so we already
know
how to work with that, let's create a brand new class for the response. So you know, this
handles
the direction of the user sending data to us. And now we want to handle us sending data to the
user.
So I can create a brand new class, we can call this post response, or because it
represents
the response, or we can just call it post, it doesn't really matter, I'm going to call it just
post. And then we're going to extend base model because all of our pydantic models have to
extend
base model. And then from here, what we have to do is we have to specify all of the fields
that
we want in the response. So we want to send back the title, because that makes sense. So we'll
send back the title, we'll send back the content, which is going to be of a type string. And
then
we'll send back published as well, which is going to be a Boolean. And let's see what that
looks like. Alright, so this still looks exactly the same as the other
ones. But if you actually see right now, for any of the one requests that we sent, we get back
the
ID, as well as the created at field. But since we didn't include those two fields, we
shouldn't
actually send it back to the user. So let's actually try this out. So I'm going to save
this,
and then to actually define the response model. Under a specific request, we'll start off
with
we'll start off with the create request or create posts. And then within the
decorator,
we just pass in another field. And this is called response model. And then you can just let VS
code
automatically select it. And then here, we just reference schemas dot, and then the name of
the class. So let's try this out. And let's see what happens.
So now if I create a post, we get a 500 error. And let's see what happened. It says value
is
not a valid dict. So it's, it looks like pedantic is saying that, you know, when we try to
send the
response, the data we got back was not a valid dictionary, right? And because pedantic,
the
pedantic class works with dictionary, it takes a dictionary and then converts it to that
specific model. So what exactly happened here? Why is this causing an error? Well, if you look at
the
documentation, and where is it right here? And if you go under SQL relational database, it
says that
for your models, your pedantic models, we have to add this extra config called class config,
or a mode true. And explains exactly why we need that. Right? Because by default, the
pedantic
model will only read it if it's a dictionary, right? And that's what it's expecting in our
code, because we see the error, it's not a valid dictionary. And that's because when we
actually
make this query, right, new post, this is actually not a dictionary, it's a SQL alchemy model.
And so
pedantic has no idea what to do with the SQL alchemy model, it only knows how to work with
dictionaries. So we have to tell it to actually convert this SQL alchemy model to be a
pedantic
model. And we do that by passing in this specific text right here. So this is going to say or
a mode
true, this is an ORM model, and it's going to tell pedantic say to, you know, ignore the fact
that
it's not dictionary, and go ahead and convert it. So we can just copy this text and add this
to our
code for our post model. And that should be properly indented. For now that should be
good.
And let's see if we still get an error now. Let's hit send. Look at that we got it. It seems
like
everything worked okay to a one created. But take a look at the fields that we got back, we
just got title content published. And that's because our model explicitly specified title content
published.
Now we know if we go to our Postgres database, we do have an ID field and a created at field.
So if we wanted to add those, we can say ID, which is going to be ever type int.
Let's save that and let's see what that looks like. Right now we get the ID back. Perfect.
Well,
do we want the created at time? I think, you know, your front end code probably wants that as
well. So let's go ahead and add that. So here we'll do created at.
Now, what should the type for created at be? Right? It is ultimately, you know, a date and
time. Well,
we can we could do, you know, something like a string. But what we can do that's better than
that is we can import from date, time import date, time. And so now we can say that the
time,
the created at is going to be of type date, time. And so this is an extra little bit of
validation
to make sure that what we send back is a actual valid date, time, just make sure that you
import it on in this file. And so now, if I send this back, we now get created at as well. Right.
So
this is how we ultimately define the data that we send back, we can specify exactly the fields
we
want. And that way, we can ensure we don't unnecessarily send data that shouldn't be getting
sent. Right. And we can do this with anything, I can just send back the ID if I really wanted
to.
Now we just get the ID, right? You see, there's a lot of flexibility and a lot of power. When
it comes to defining your responses, just like we had when it comes to defining our requests.
And
with API's, like I always say, make sure you explicitly define the exact data you want to
receive, and the exact data you want to send back to the client. Now, before we move any
further
and wrap up this video, you'll see that there's a lot of duplication, right? Because this post
model
for the response, right, it needs title content published, and then we're just adding ID
and
created at, right. And like I said, when it comes to a real application, you're going to have
so many more fields, so many more columns, it would be such a pain to have to repeat both of
them.
And so that's why I ultimately created this post base class, because I can extend the post
base class. And what that's going to do is, is that's going to cause me to inherit the title
content
and published fields, because they're already defined in here. And so I can just remove
that.
And so then I can just specify ID and create it at, and then it's going to inherit the other
three
from the previous class. And so now if I try this again, I send it, you can see that I get
the
same exact result, but I only had to specify the new columns I wanted to add in the
response.
So this is really no different than, you know, working with any other model in Python, we get
access to all of the usual features like inheritance, so we can help reduce the amount of code that
we
actually write. All right, and so now let's go ahead and update all of the other path
operations
as well, because we can see that we only did it for create posts. But let's go ahead and do it
for get an individual post. So this is going to be the same exact response model. And we can
just
reference schemas dot post. And we'll do the same thing for our update post as well. We'll
say
response model equals schemas dot post. And then let's just, you know, double check. So get
one
post, this should work just fine. Perfect. And then update post. And it looks like that
works
as well. And the last thing that we need to do is get all posts. Right. So right now, if we do
get
all posts, now, where is that right here? We still haven't sent us a list to response model
equals,
and then we can say schemas dot post. And then let's try that out. We hit send, we get an
error,
and this is expected. And I want you to stop and whoops, I want you to stop and think about
what
exactly is happening. Because we are returning a list of posts. And it's trying to shape that
into
one individual post, right, we got a list of posts, we should be sending back a list of our
schema
post. So how do we do that? Can we just put a could we just put a bracket or something like
that?
Will that work? Let's try this. Well, it looks like there's an error. So this doesn't seem
to
work. So how can we specify we want a list of posts? Well, we have to import something
from
the typing library. So we have an optional from the typing library, we can also import list.
And
so now I go back to our get posts, I can say, list. And so now if I hit send, we should get
back all
of our posts. So here, we're just specifying we want a list of our specific schema post
model.
And that's just going to allow us to get a list of posts. Very simple. In the next couple
of
sections, we're going to focus on creating user functionality. And what I mean by that is
having
users be able to actually create an account within our app, being able to log in, as well as
being
able to create posts that are associated with their specific account. And so the first thing
that I want to tackle is handling user registration. So we need to be able to provide a way for
users
to create a brand new account. And the first thing that we have to do is we have to define our
model, or more specifically, we have to create a table within our Postgres database, that's going
to
hold all of our user information. And so since we're using SQL alchemy, we're going to
create
a new ORM model, just like we did for post so that we can define what our Postgres table
ultimately
looks like. Let's create a new class. And I'm going to call this class user. And this is
going
to extend base, that's just a requirement for any SQL alchemy model. And then here, we're
going to provide the table name within Postgres. So I'm going to say the table within Postgres
should
be called users. Then let's go ahead and figure out what are the different columns that we
need.
And so we're going to handle registration by having the user providing a email
address.
So we should have a column for us to be able to store their email. So we'll say
email
equals column, this is going to be of a type string. And we're going to say that this is
nullable is set to false. So they have to provide an email.
And then on top of that, we shouldn't allow someone within with a specific email to
register
twice with that email, right, there should only be one account with one specific email. So
we're going to say that the unique constraint is set to true, that's going to prevent one email
from
registering twice. The next thing that we need is for us to be able to store the user's
password.
So we'll create a column for password. This is once again going to be a
string.
And then once again, the nullable is going to be set to false, because we shouldn't be able to
let them create an account without a password. And we don't need a unique constraint, because I
don't
care if two users have the same password is up to them. And then finally, just like we had
with the
posts class, we're going to have a column for ID. So every user is going to have a unique ID.
So
I'm just going to copy and paste that. And so at this point, I think that's all we need for
our
user class. If you save it, it's going to reload the application, and it looks like I got an
error.
And I realized this should not be str, this should actually be string. That's my
mistake.
And then actually, before we do anything else, I want to create a that column as well. So we
can
just copy this, and then paste that in there, like I said, pretty much anytime you create
something in your database, you want to make sure that you record when it was created, you never know
when
you'll need that information. And so we've restarted our application, let's go to Postgres.
And under
my tables, I'm just going to do a refresh. And it looks like it went ahead and created our
users table. And if we do a user's properties, we should see that it created all four columns. And all
of
the necessary constraints should be set there as well. And so let's, you know, just play
around
with this in Postgres just to make sure it works. Because I don't really trust SQL alchemy
ever. So let's do right click. And then we'll do view edit data. So right now there shouldn't be
any
users. And let's create a new user. So I'll say this is john at gmail.com. password, you
know,
just put whatever you want for now. And then if we save that, right, it then successfully
created
a user, we got the generated ID as well as the created at timestamp. And then let's create
a
new user. So I'm going to use the same exact email. So this should throw an error because we
shouldn't be able to register again, you know, some random password. And then if I try to
save
this, we should get a unique constraint on users email error. So perfect. So that worked. And
I'm just going to change this to be Cindy at gmail.com. We'll save that. And it should create those
users
self perfect. So we've got the first step done when it comes to defining our table. The
next
thing that I want to do is we will create a new path operation so that a user can
actually
send his username and password to our API. And then we can actually generate that
user.
Just like we did with posts, we're going to create a new path operation for creating a new
user. So we'll go to the bottom of our main document. And I'm going to create a new
function. I'll say we'll call this create user. And so just like we have for creating a
post,
I'm going to copy this decorator, and we'll rename it. So it is going to be a post
request,
but it's going to be sent to the URL of users because we're no longer working with posts. And
keep in mind, you get to select whatever URL or path you want to use. If you want to call
this,
I mean, it's bad practice to call it create user. But you can choose whatever you
want.
I think it makes sense to call it users. And anytime you create something,
remember,
the status code should always be the default 201. And I'll get rid of the response model for
now
just to keep things simple. Now, we're ultimately going to be using our database to create a
brand
new user. So let's go ahead and copy this DB right here from all of the other path
operations,
because that's going to have to go in there. And then anytime we want to, you know, receive
data from the user, because the user is going to have to send the email he wants to register
with,
as well as his password in the body of the request, it always makes sense to define a specific
schema
so that we can ensure that the user does provide both of those. So just like we did with
posts, we're going to create a brand new schema. And I can call this user or we can call this
user.
I think this one, it makes sense to call this user create. So this is going to handle just for
creating users. And we're going to inherit from base model. And for the user create,
there's going to be an email field, which is going to be a string, as well as a password
field,
which is going to be of a type string. And when it comes to validating the user,
right,
it's going to check to make sure that an email is provided and a password is provided.
However, we can get a little bit more granular than that. If you actually go to the pedantic
documentation,
we also have a field called email string. But this is going to validate that the email
property is a valid email. However, we need to have the email validator library installed. But that
should
automatically have already been installed for us when we installed fast API with the all flag.
And so if you do a PIP freeze, we should be able to see that we have our email validator
already
installed. If you don't have it installed for some reason, just go ahead and do a PIP
install
email validator. And that should install that library. And so what we'll do is from
the
pedantic library, we're going to import email string. And then we can say this is going to
be
email, a type email string that's going to ensure that this is a valid email and not just
some
random text. Then back in our main.py file. Just like we did before, we can say
user,
and then this is going to use schemas dot user create. And so the email address and the
password
is going to be stored in an object called user, there's going to be a pedantic object as
well.
And if you forgot how to save data to the database or create something with SQL
alchemy,
just go ahead and take a look at the create posts. And we can essentially just copy this. And
we're just going to rename a few things. We'll say new user. And then we're going to
grab models dot user now. And then once again, what we want to do is we want to take the
user
that we get back from here from our schema, we want to convert it to a dictionary and then
unpack that dictionary. Then we're going to add it to our database. We're going to commit
it,
then we're going to refresh it. So we see the brand new user. And then we can go ahead and
just
return new user. Alright, let's give that a shot. And then within our postman, we can right
click
on this, select Add request, we're going to name this create user, this is going to be a post
request.
And then in the body, we'll go to raw once again, and then JSON. And then here we can pass in
the email. We'll give it some random email. So I'll call this
Carl at gmail.com. And then password. This will be password 123. All right, and then for the
URL,
just copy posts. And then here, we'll just change this to users. All right, so let's give this
a
shot. Let's see if this works. And it looks like it worked, right? We got the email, we got
the
password back, we got the created at and we've got an ID. But just to double check, like we
always do go ahead and go to your users table, select query tool, select star from users now.
Because
we're now querying, no longer the post table, we're querying the users table. If I run
this,
we can see that we do get the new email that we created or the new user that we created with
Carl at gmail.com. Now as a quick test, just to make sure that email validator works,
I'm going to change this. And we'll just say some random text, this is obviously not a valid
email.
So let's see if it properly creates that user. And it looks like our scheme of validator
worked and
says value is not a valid email address. So you can see the powerfulness of the pedantic
library being able to automatically check to see if that's a valid email address. All right, but one thing
I
didn't like about creating the user is that if we change this back to a valid email, I'll
say,
just call this new user at gmail.com. We create that user, you see that he gets his password
back.
Now, why in the world would he want to see his password back? There's no reason to ever send
the password back to the user at any point. And so one of the things I want to do is I want
to
define our response model so that we never send back the user. And I think you guys should
already be able to do that. But we'll walk I'll walk you through how to do that. So let's go to our
schemas,
we've got our user create, let's create our let's create a new class, and I'll call this user
out.
And so this is going to be the shape of our model when we send back the user to the client
that requested it is going to extend base model as well.
And here, we're going to send out a couple things. So there's going to be an ID now, the user
should know his ID, email. That's going to be of email string again.
And then we'll just leave out password. So by leaving out password, we won't ever send it
back. But just like we did with the post model, remember, this is going to be a SQL
alchemy
model that we get. And we need pedantic to convert it to a regular pedantic model. So we need
this config right here. And then in our path operation, we can set the response model to be schemas
dot
user out. All right, and then now if I just change the fields up a little
bit,
create a new email, you can see that we get the ID and the email as well, but there's no
password.
And that's exactly what we want. And so now you guys should be fairly comfortable with being
able
to define a response model as well as in a request model so that you can pick and choose
whatever field you want. One last change I want to make is I want to go ahead and just add the created
at
field as well. And we can just copy this right here. So now just create another email.
Right.
And now we get the creative field. So we got all the fields that we want. In the last lesson,
we handled the logic for creating a new user. But for a lot of the people that are
familiar
with security, I'm sure you guys may have had a little bit of a heart attack. Because we did
something that's very, very frowned upon. We took the user's password, and we just stored it
as
plain text within our database. And some of you might be thinking, Well, what's the problem
with that? Right? It's in our secure database. Well, right, our database is secure for now. But
there
are hacks, there are things that can happen, you know, these, all of these records could
potentially get leaked in some fashion. And it's very dangerous to just have a user's password just stored
in
plain text like this that anyone can read. So when it comes to working with passwords and
databases, what you always want to do is you want to hash the password. So we never store the actual
password
in our database, we just store a hash of it. So that if it does get leaked, well, no biggie,
because it's ultimately just a hash, you can't really reverse engineer that hash and get
the
original password back. So in this lesson, we're going to focus on hashing the password when
a
user first registers so that we never actually store the raw password within the
database.
And within our fast API documentation, they have a good article that kind of covers how to do
that.
So if you go under security, and then OAuth two with password, it'll show you what we actually
need to do when it comes to hashing. So there's two libraries that we need to install.
Right,
we need past lib. And so that's going to kind of handle the hashing. But we need to
actually
specify a specific algorithm because past lib can work with different algorithms. One of the
more popular ones is bcrypt. So we'll need two different libraries, we'll need the past
lib,
and then the bcrypt library. And we can just do that by running pip install past lib bcrypt.
So I'll just copy this will go to our application. And I'm just going to run that
command.
Right, and then let's just do a pip freeze just to make sure it got installed. And we see past
lib there now. And then do we see bcrypt? And we see bcrypt in there as well. So
we've got both of the libraries that we need to actually perform the hashing. So let's go to
our
main file. And somewhere up at the top, I'm going to import from past lib dot context
import
crypto context. And then what we need to do is come place below that we have to
define
this setting right here. So we do pwd underscore context equals, and then we reference that
crypto
context. And then here we say schemas equals brackets, bcrypt. And then we'll do
deprecated
equals auto. Alright, and so basically all we're doing is, and this is going to actually be
a
string. All we're doing right here is we're telling past lib, what is the default hashing
algorithm,
or what hashing algorithm do we want to use? In this case, we want to use bcrypt. So that's
all this is doing. Now, when we go to our user registration, down here, we have to do a
couple
things. And so before we actually create the user, we need to actually create the hash of the
password.
So the first thing is what's not in JavaScript land, we're going to hash the password,
which
can be retrieved from user dot password, right? Because that's going to be stored inside this
object. And so how do we exactly do that? Well, what we can do is all we have to do is
reference
that password context, right? If you forgot what that is, just go all the way up to the top.
That's this command right here. So we reference password context. And we call the hash
method.
So that's going to perform a hash, and then we just pass in user dot
password.
And then we can store this in a variable called hashed underscore
password.
And then what we're going to do here is we're going to take the user dot password. And
we're
going to set that now to the new hashed password. That's going to update the pedantic user
model.
And then at that point, we can just leave everything as is, right? So we hash the password, so
we got a hash, and then we stored it under user password, and then everything else can just be
kept
the same. So let's actually try this out. And let me just double check to see there's no
errors, and there isn't. So let's create a brand new user. And I'll call this mark at
gmail.com.
Same password, that's fine. Then it looks like everything worked. And then let's just run
this.
And then if you see that most recent user, you can see that we no longer store the raw
password,
we store a hash of it. And so that's going to help it be a little bit more secure. Because if
it does leak, then you know, the hackers will only get access to a hashed password, and they
can't
exactly convert it back to the original password. That's the great part about hashing. It's a
one
way street, we hash it, we only get the final password, you can never put it back into a
function to convert it back to a password. Now, one thing I want to do is I want to extract all of
the
hashing logic and store it in its own function. So I'm going to create a new file. And I call
this
utils.py. These are this file is just going to hold a bunch of utility functions. And what
I'm
going to do is I'm going to go to our main file. And I'm going to remove this line or cut
it.
And we're going to paste it into my utils file. And then we're also going to cut this out.
We're going to move all of this logic into here. And then we're going to define a function
that
we can call. And I'm going to call this function hash. And it's going to take a password,
which
is going to be of type string. And all it's going to do is it's going to return pwd context
dot hash
of whatever the password the user passes in. And that way, we really extract and place all of
the
hashing logic into one file or one function. So we don't have to import all of this nonsense
into other files. And then in our main.py file, we can import utils. And then
down here, what we can do is instead of calling this, I can just call utils dot hash. And
then
we'll pass in the user dot password again. And this shouldn't really change anything else in
our code.
Let's try this again, create a just add a one here. Then seems to work. Let's just double
check.
And the second user, the password was properly hashed. The next thing I want to do is I want
to
set up a route or a path operation that allows you to fetch and retrieve information about a
user based off of their ID. And there's a couple of different reasons why I want to do
this.
One of them is it can be part of the authentication process. So depending on how you set up
the front end, if you decide to, you know, set up JWT tokens to get sent as cookies, then the front end may
not
actually know whether it's logged in or not. And so a lot of times you'll see some APIs have
an
endpoint to let you kind of retrieve information about your own account. And so if you're able
to
access it, that means you're logged in. If you're not, then you know that you need to fetch a
new token. Also, there's other reasons why you want to do this, too. You know, if you're, you
know,
taking a look at something like Twitter, right, you need to retrieve a user's profile if you
want to view it. And so in your API, you're going to have to set up a route so that people can
retrieve
someone's profile information. And so that's why I want to set up this route. I haven't really
decided what we're going to ultimately do with it. But for now, I just want to set it up so that
you
can retrieve information about a specific user based off of their ID. Let's go to the
bottom
of our code here. And we'll create a new path operation. And I'll call this get user.
And
before we do that, I'm also going to set up the decorator. So this is going to be a get
operation because we're going to retrieve the information about a specific user. And the specific path
is
going to be slash users, and then slash ID. So kind of like how we did when it comes to
retrieving
a specific post, we're going to pass in the ID in the URL. And then we have to extract the
ID.
And I'm going to make sure that we validate it as an integer. And the next thing that we have
to do is since we're going to be interacting with the database, we need our DB right here. So
we'll
copy that. And then in our actual function, we're going to do a quick query. So we'll do DB
dot
query. And then we'll grab models dot user. And I'll say I want to filter and then look
for
something with models dot user dot ID equals equals, and then it's going to match up with
the
ID that they requested. And because there should only be one user with a specific ID, we're
going
to just grab the first one so we don't spend extra resources looking through the rest of our
database. And we're going to store this in a variable called user. All right, and then just like we did
for
fetching a specific post, if a user was not found, so if not user, we'll say we want to raise
an HTTP
exception. And we'll say the status code equals 404 whoops, HTTP status dot and then we'll
grab
404. And the detail is going to be user with ID, and we'll pass in the ID does not exist. But
if
the user is properly found, we will return the user. And it looks like there's some kind of
weird
issue with my formatting. So all right, there we go. And so let's just quickly test this out.
So
once again, I'm going to create a new request. And we're going to make sure to save this one.
And I'm going to call this get user copy this URL, it's going to be the same,
same route is going to be a get operation. And then we want to send a specific ID. So I take a
look at your database, and then see if you can grab an ID that does exist. So let's
just
say one, I'll hit send. And it looks like there's some kind of issue because I'm not getting
a
response yet. Let me cancel out of that. And let's see what we did wrong. And I realized I
forgot to
do app dot get. Great, so we actually got the user everything seems to be working.
However,
there's one little issue. First of all, we should not be getting the password, right? We never
want
to return the password to the user, the user already knows his password. And also if you want
to if this route is so that you can retrieve a user's profile, kind of like you can on
Twitter,
Instagram, then we're sharing someone else's password. And that's a little bit of an issue.
And the reason why this isn't a hash password was because I didn't clear out my database before
we
implemented the the hashing of the password. So that's why it's in clear text. But whether
it's hashed or not, you never want to send this out, right? You don't want anyone else to see
your
password, you don't even want the user to see their own password, because someone else could
intercept it and potentially do malicious activity with that information. So what we're going to
do
is we're going to filter out that specific field, just the password field, I want them to be
able
to get the ID, the created that field and the email and any other fields outside of the
password.
And fortunately for us, if we go to our schemas, we've actually already defined
a
a schema for the user out. So this is going to be any information about the specific
user,
except we're extracting out the specific password. And so we can just set the response model
to be
user out. And we'll go back to main.py and we'll say response dot model. Sorry, response
underscore
model equals schemas dot user out. Alright, we'll save this and we'll try this again. So
we'll
retrieve this user. And we still got the password. So what happened here? And this could just
be an
issue of me not saving. So let's try this again. Still a little bit of an issue. And let's
just
make sure I imported schemas. I did. And let's go to our schemas. And I've set just these
fields. So
this should be good to go. I'm not sure why it's giving us errors. And I made a stupid
mistake,
this should not be within the function parameters, this should actually go inside the
decorator.
So it wasn't actually doing anything. And now if we try this, we can see that the password
field has been successfully removed. Now that we've added a
couple of path operations for users, if you take a look at our main.py file, it's starting to
look
a little cluttered. And you'll see that we've got all of our path operations for, you know,
handling
crud operations for posts. And then you'll see that we also have all of our path operations
for
working with users. So that's creating users, as well as retrieving a user by ID. And, you
know,
as I said, this is a little messy. And as we keep adding more and more path operations, it
just seems almost unmanageable to keep everything in a main.py file. And instead, what I want to do is I
want to
break it out. And I want to create two separate files. And one file is going to be for all of
the
routes or path operations that work and deal with posts. And then I want a separate file that
will
handle all of the path operations for working with users. And, you know, it's not quite as
simple as
just, you know, moving these path operators into different files, we do have to learn about
something specific to fast API. And this is nothing unique about fast API, you'll see
that
every single web framework is going to have a way to kind of accomplish this. And it
usually
involves something called routers. So we'll take a look at routers in this section and how we
can use them to actually split up all of our path operations so that we can organize our code
a
little bit better. And so what we're going to do is we're going to create a new folder. And
I'm
going to call this routers. And then within here, I'm going to create two files, I'm going to
create one called post p y. And I'm going to create one that's called user dot p y. So all of our
path
operations, dealing with users is going to be put into this file. And all of our path
operations dealing with posts is going to be put into this file. And what I'm going to do is we're going
to
go to remain dot p y file. And I'm actually just going to copy all of my specific routes that
deal
with posts first. We're just going to keep going up and I went too far up. Oh, this thing
moves
quickly. Here we go. This is going to grab all of our posts. And we're just cut that out. And
we're
just paste it in here. And there should be plenty of warnings and errors within VS code.
That's to be expected. Don't worry about that. We'll fix that in a bit. I'm gonna do the same thing
for
the users. So let's go to our main dot p y file, and just grab the last two path operations
that we have for users. And then paste it into this specific file. Right, so we've got a bunch
of
errors. And let's go ahead and actually fix those first before we actually start working with
routers.
And so now that all of the the path operations are within these files, we're going to have to
import,
you know, things like app, we're going to have to import, well, we'll come back to that,
actually, we'll have to import status, we'll have to import our schemas, we'll have to import the
session
object, we'll have to import all of our database related things, our utils. So all of these
things that we see little squiggly lines, we have to import them, because we had already imported
all
of these in the main dot p y file. And so we'll start off by importing the models, the
schemas,
and the utils folder, if we need it for one of the specific files, you'll see that we only
need this for the user file. And so the way we did it in our main dot p y file is we say from and
then
this dot means current directory. So we say hey, from this current directory we're in, which
is if
I kind of close out, you can see that everything is in the app file app folder, sorry, we're
saying
from the current app directory, I want to import models, which is right here. And I want to
import
schemas and utils. However, in our user dot p y file in our post up p y file, the model
schemas
and utils folder is not in the same directory, because we actually have to go up a directory
to the app folder to access them. So instead of doing one dot, if you want to go up a
directory,
you do two dots. So that's all we have to change. So in our user dot p y file, we'll say from
dot dot import, and we'll say models, schemas, and utils. And so right there, we cleaned up
some
of our errors that we're getting. And let's see, we also have to import status from the fast
API
library. So what we can do is we'll just go up here. And I'm just going to copy this. And
right
above this line, we're just going to paste that in there. And the last thing that we need to
import is from the database, we are from the database dot p y file, we're gonna have to import get
DB,
and then from SQL, alchemy, we have to import session. So if we go to our main dot p y file,
again, the SQL alchemy code right here, we can just copy that. And I'm going to import that as
well.
That's done. And then finally, we have to get DB. And if we take a look at our main dot p y
file, the way we got that is we're saying, from the current database file, we want to import
engine
and get DB. And keep in mind that the database file is not in the routers directory, it's in
the app directory. So we have to go up a directory to access this file. So we just put two dots
pretty
simple. So from dot dot database, import, get DB. And we actually don't need to import
engine,
we're going to leave that here in our main dot p y file. Alright, so now that we got all of
that,
the last error that we see is that we don't have access to the app object. And so your
instinct is
to go to our main dot p y file and then import this. And that's not exactly correct.
Instead,
what we're going to do is we're going to make use of the routers that I mentioned. And so from
the fast API library, we're going to import something called API, first three letters, actually,
the
first four letters API router. And then from here, we're going to make use of the API router
object.
So we'll say router equals API router. So we're basically creating a router object. And then
what
we can do is we can replace the keyword app, because we don't have access to that in this
file. And we just use the word router. And you'll understand why we do this in a bit.
So we've got those updated. And then we're gonna do the same exact thing for our post API. So
we
have to get all of these imports, I'm actually going to copy most of these, because most of
them are going to apply here as well. The only thing we don't need utils, for sure. So we'll remove
that.
And then you'll see that list is still getting aired out. And that's because if we go to our
main.py file, that's coming from this typing import. So I'm just going to copy that and
paste
that in there. And it looks like we don't need optional because it's kind of grayish. And so
that's just a VS code telling us that we're not using it in this file. And I think that
should
clear up all the errors except for once again, the app object. And so we'll do the same
thing,
we'll say a router equals API router. And then what we're going to do is replace the word
app
with the word router. All right, and then finally, we have to go to our main.py
file,
and actually make use of these routers. Because you'll see that in our main.py file, there's
no reference to anything in here. So our API won't work if we try to run it now.
So in our main.py file, what I'm going to do is I'm going to say from dot
routers.
So from this folder, we're going to import post and user. And this should be plural
routers.
And then I'm going to show you some magic that we're going to do. And so above this route,
we're going to say we're going to grab the app object, right, this is once again, our fast
API
object that we kind of do everything with, I'm going to say, include router. And then we'll
grab
the post dot router, right, that's what we just imported, we imported post, which is coming
from
here. And we're importing this router object. And so what we've basically done here is, I
have
basically said as we, you know, when we get a path, when we get a HTTP request, you know,
before we
had all of our path operations in here, instead, what's going to happen is, you know, we go
down the list like we normally do. And so as we go down our list, this is our first app object that
we
kind of reference. And in here, it just says, I want you to include everything, I want you to
include our post dot router. And so the request will then go into here. And it's going to take
a
look at all of these routes. And it's going to see if it's a match. And if it finds a match,
it's going to respond like it normally does. So that's kind of how we break out our
code
into separate files, we use these router objects. And we can do the same thing with the
user.py.
So we're going to go to main. And then here, I'm going to type out the same thing, I'm going
to say app dot include router. And this time we use, we grab the user, and then we do router
again.
And so once again, all we're doing is we're just grabbing the router object from the user
file. And that's essentially going to import all of the specific routes.
And so if we save our code, and then we just quickly take a look at the
terminal,
just to make sure there's no errors, it looks like everything's working. Let's test this out
now. So I'm going to get all posts. Let's see if this works. It looks like it works great. Let's
create
a new post. Looks like it works. Get one post. Well, it looks like that post doesn't exist
anymore.
But if I try one, looks like it works. I will delete post with the ID of three. Great. We'll
update
post, create user, just put in a new unique email, and then get user. So grab that user. And
we can
see that every single route works perfectly. So we didn't change the functionality of our
project at
all in this video. Instead, all we did was we used a a router object to be able to spit split
up all
of our routes or path operations into different files. And then we import them just by
calling
app dot include router and then the specific router object of that file. And you'll see that
our code looks so much cleaner now. And as our app and our API continues to grow, we can
just
add new files into the routers folder so that we don't continue to clutter up our main.py
file.
In our two router files, you'll see that we use the same URL for almost all of our
specific
paths. And so you'll see for the get route, we'll use slash posts for the post, we use slash
posts
from the get specific posts, we do slash posts, and then the ID and then the same thing goes
for delete and input as well. And I think it's kind of annoying having to continuously copy and
paste
the same exact path when it's pretty much just a copy from each one. So is there a way we can
kind
of remove this unnecessaryness? It doesn't seem like that big of an issue. But keep in mind
that
our API is very simple, right? Other API's could have very long, complex, you
know,
routes that, you know, may not look as simple as ours, so they could have, you know, multiple
things, right? So it could look something like that. And then having to kind of copy and
paste
all of that every on every single route can seem a little unnecessary. And so anytime you're
working
with routers, what we can actually do is we can pass in a parameter into the API router
function
or method. And so what we can say was prefix. And we're going to say that since every
single
route in this file always starts with slash posts, we can say prefix equals slash
posts.
And so now anytime you see a slash post, we can just remove that
and just put in a slash, this just going to be a slash.
And then when it gets to slash post slash ID, this is where it gets a little tricky. But once
again, we just remove everything but a slash. And so what this is saying is that we're going to take
slash
posts. And then we're going to append it with ID, right? Because that's, that's what this is
saying,
we're saying slash ID. So it's actually appending it with slash ID. So the final thing
actually looks like, it's going to look like slash posts as ID. So nothing actually changes. It's just
a
simpler way to do things so that we don't have to write slash posts everywhere. And so we
can,
we can remove this and just do slash ID. And we can just do slash ID here as
well.
And we're going to do the same thing in the user's file as well, because both of them start
with slash users. And so we're going to put in a prefix here. And we're going to say
slash
users. But this can just be removed to slash. And this can be removed to just slash ID. All
right.
And once again, we're going to test this out, we're just going to test a couple of routes. So
if get routes, get posts work. And if create, I will try to get one post. That works
perfectly.
And then let's try creating a new user, we have to give it another unique email. Looks like it
works. And then finally, get user, we're going to try this again. And it looks like it all
works.
So you know, this is a completely optional step, it doesn't change the functionality of
things. But I think it does make it a little bit easier to read. Because we've put the prefix up here
so
that we don't have to keep copying and pasting. Now, as I mentioned, one of my favorite
features
of fast API is the automatic documentation that comes from swagger UI. And so if you
actually
navigate to the URL, but just go to slash docs, like I have, that's where you're going to see
the documentation. And like I said, this is interactive documentation. So you can actually make
requests
from here. So you didn't technically have to use postman for a lot of things, you can just
test it out right here. And it's going to do the same thing. However, I did also want to teach
postman.
But the reason I bring this up again is that you'll notice how we've got a couple of
routes
for dealing with posts. And then we've got a couple of routes for dealing with users. And then
we've got this random test one that we created when we were first learning about path
operations.
And what I would like to do is I would like to structure this documentation so that instead of
kind of grouping them all together, we can kind of group them based off of the their
responsibility. So I would like to create a group that handles all of the post operations. So
all of
these five right here, and then I would like a separate group that handles all of the user
based operations, so that it's a little bit easier for your clients and your users to understand
what
each one does by grouping them accordingly, based off of their specific action. And so I want
to
have a group that actually is titled posts, any group that's titled users, so that when they
see that they say, Oh, the user section is going to be dealing with users in the post section is
going
to be dealing with posts. And getting this fixed or update in our code is super simple with
fast
API. So let's go to our fast API, go to our routers. And if we go to the post.py, to add a
specific
group name, what we call, it's actually called a tag. So we say tags equals, we just say
posts.
And I've got that comma right there. And we do the same thing for users. So we'll say tags
equals users.
And you see, you could see that this is a list so you can pass more than one. But we'll save
that. And then I'm going to refresh this page.
And so now you can see that we now have these little titles so that we can now group our
specific
requests into categories. And so the readability of our documentation has improved
tremendously
with essentially one line of code. In this section, we're going to start tackling one of the
most
important topics when it comes to building out an API or really any application, and that is
authentication. Now when you're working with authentication on an API, or any
application,
there's really two main ways to tackle authentication. There's the session based
authentication. And the
idea behind a session is that we store something on our back end server or API in this
case,
to track whether a user is logged in. So there's some piece of information, whether we store
it in the database, whether we store it in memory, that's going to keep track of if the user has logged
in
and when the user logs out. So that's one way of doing things. The other way of doing
things
is doing is using JWT token based authentication. And the idea behind JWT authentication is
that
it's stateless. And what I mean by that is that there's nothing on our back end, there's
nothing on our API, there's nothing in our database that actually keeps track or stores some sort
of
information about whether a user is logged in or logged out. We and you're probably thinking,
well, how do we know that they're logged in? Well, that's the power of JWT tokens is that the
token
itself, which we don't store in our database, and we don't store in our API, it's actually
stored on the front end on our clients, actually keeps track of, you know, whether a user is
logged
in or not. And when I first started learning about JWT, it was a little bit of a
complex
topic, it seemed like there were so many pieces that were in play. But then when you when I
actually stopped to really understand it, I realized it's one of the simplest solutions.
So
hopefully, I can make this as easy as possible for you guys. I hope this doesn't end up kind
of confusing you like it confused me at first, it really is simple. And there's really only
a
couple of steps. But you got to really understand what a JWT token is and what we're doing on
the
front end and the back end to make it so that we can actually use it as an authentication
solution.
So let's take a look at the flow for how a user logs in, how a user is essentially
authenticated,
and then how a user accesses a specific path operation resource or endpoint by using the
JWT
token to ensure that the API knows that we're logged in so that we can actually provide him
that important information. And so what's going to happen is the client or the front
end,
whoever it is, they're going to try and log in. So what we're going to do is we're ultimately
going to create a path operation, call it slash login, and the client is going to pass
the
username, and they're going to pass in the password. So whatever their credentials are, so it
doesn't technically have to be username and password, it can be email and password,
it can be whatever information that you want. So technically, in our application, it's going
to be email and password, because we don't have usernames, I guess,
we mostly just have emails, but they provide their credentials. And so after we get
their
credentials, what we're going to do is, first of all, we're going to create well, first of
all, we're going to verify if credentials are valid, right? So if the credentials are correct, if
the
password matches the if the username and password match with the account, we're going to
create
this JWT token. And we haven't talked about what the JWT token is. So don't worry too much
about
it. But I've created an example token right here. And so you're probably thinking it just
looks like a bunch of gibberish. And for the most part, it is. So moving forward, I want you to
think
of this as nothing more than just a string, you know, just a regular string in Python, that's
kind of fundamentally what it is. But there's a little bit more information embedded
in it. But think of it as a string from the perspective of the client, the client doesn't know
what the token is, and it never cares really the only the API cares, what's actually in
the
token and what it means. And so we'll send a response back with the token. And so now
the
client has the token. And so he can start accessing resources that require authentication. So
anytime
he wants to, you know, let's say our application requires a user to be logged in to retrieve
posts, what he can do is he'll send a request to the slash posts endpoint. But he also
provides
the token in the header of the request. And you may not know what the header of a request
is,
but I'll show you guys, it's very simple, you could just think of it, it's somewhere in the
payload of the request. And so he sends that. And what the API is going to do our fast API
is,
first of all, it's going to verify the token is valid. And there's a couple of different
steps
needed to actually verify if a token is valid. And we're going to cover that in the next
slide, but just know that the API just checks, hey, is this a valid token? And if it is,
well,
then he just sends back the data. And that's it. All right, our authentication system is dead
simple. You provide your credentials, you get a token. And then anytime you want to
access
anything that requires you to be logged in, you just send the token in the header. And that's
it.
So hopefully this wasn't confusing. Hopefully you guys see this simplicity in the solution.
The API doesn't actually track anything. There's no information being stored on the API.
Instead,
the client just holds on to the token. And he provides it to us. And if we verify that the
token is valid, that's all we have to do. So let's break down what exactly a JWT token is. And
what
are the components that make up a token. And so I've got an example token right here. And this
is kind of the same piece of text that we saw in the previous slide. And so it once again, just
looks
like a bunch of cryptic characters just jammed together. And that's kind of what it is. It
looks like it's encrypted. But keep in mind, it is not encrypted. This is important to understand.
And
we're going to make sure I drive home this point, it is not encrypted. But the token itself is
made
up of three individual pieces. The first thing is the header. So the header includes metadata
about
the token. And so we're actually going to sign this token. So we're going to think of it
almost
like hashing the token. And so we have to specify the algorithm that we're going to use. So in
this case, the default is HS 256. So we've included that in the metadata. And then you can see
the
type is set to JWT. So this is a JWT token. So that's why the type is set. So the metadata
is
going to be the same for all of our tokens. This is just kind of fixed, don't worry too much
about it. It's not really that important. We don't really ever touch it. Now, the payload is a little
bit
more interesting. So the payload of a token is ultimately up to you, you can send absolutely
no
payload, you can send any piece of information that you want to send within the payload,
you
conclude anything that you want. However, you want to be careful with what you put in the
payload, because it's important to understand that the token itself is not encrypted. And so that
means
anybody else in the outside world can take a look at a token, and they can see what's in the
payload. So you don't want to put any confidential information you want, you don't want to put
any
passwords or secrets, or anything like that. Instead, you want to stick to some very
basic
things. So very common things that we put in the payload are what is the ID of the user,
right? So
when I log in, right, normally, our API is going to create a token and then embed my user ID
into
the token. So that when I asked to get all of my posts, the, the API will be able to take a
look at
the token, verify it's correct, extract to the payload, and it'll automatically know the ID of
the user that requested this. But we can include other things, we can include the user's role.
So
are they an admin? Are they just a are they a privileged user? Are they a regular user? We can
technically include any information. One thing to keep in mind is that anytime we need
to
access anything authenticated, we have to include this token. So if we jam a lot of
information in there, well, it's going to increase the size of our packet. And that's going to be a waste of
some
bandwidth. So you don't want to jam too much information, just a couple of small things here
or there. And then finally, we have a signature. So a signature is a combination of three
things.
We've got the header. So we take the header that's already in the token, we take the payload,
that's already in the token. And then we add our secret. So there's a special password that
we're
going to keep on our API. This is only on our API, our clients will not know it, no one else
will know it. And it's probably the most important thing to our whole authentication system. So
you
don't ever want the secret to get out. But we take those three things, the secret, the header,
the payload. And then we essentially take that information, pass it into the signing
algorithm,
which is the HS 256. And then it's going to return a signature. And this signature is
important
because we're going to use this to determine if the token is valid, because we don't want
anyone else tampering with our tokens, we don't want them changing data, you know, I don't want
some
user to log in, and then just start changing numbers in here to make it seem like it's a
different user, because then they can potentially access other users information. And so that's why
we
have this signature is just to make sure that no one has tampered with our specific token. And
so,
you know, I just want to drive home that same point. First of all, there's no encryption. So
anyone can see the data, the signature is just there for data integrity. And what that means
is
no one has changed it. That means all the data that we have in there is still what it should
be, no one has messed with the data. Alright, and if you're still confused as to what a signature
is,
we're going to take a look at how signature work, why we use them and how they ultimately help
us detect if someone changes any of the information in our token in the next slide. And then I
think
at that point, you guys will see it's not really a complex concept. And once you get that,
you'll
see that the whole authentication process is fairly simple. Okay, guys, so we're going to do a
deep dive into why we need the signature within the token. And you'll see this slide may look
a
little complex, there seems to be a lot of boxes, a lot of things going on. But as soon as I
explain this, I think you guys will see that it's not too complex of a concept. So let's break this
down
step by step. Let's say a user has logged into our application, he sent our credentials, and
our API has created the token, and it's in the process of sending it back to the user. So this is going
to
be our token that we create on our API. And it's going to create, it's going to have three
things, the same three things we discussed in the previous slide, we've got the header, that's got all
the
metadata, it's got the payload, right, and this is going to have whatever information we want
to put in the payload. And in this case, we've decided to embed the user's ID, as well as his specific
role.
In this case, he's a plain user, he's not like a special privileged user or a staff user or
an
admin, he's just a regular user of our application. So he can only do very specific things.
And then we have to generate a signature. And our signature is going to be a combination of
three
things, it's going to be a combination of a header. And remember, this header is the same
thing. So even though our token already has the header, what we do is we take that header, we take the
payload
with the specific pieces of information we've included in there. And then we include finally,
our special super secret password. Remember, this password only resides on our API
servers,
nobody else has access to it, our clients don't have access to it, the front end doesn't have
access to it, only we have access to it on our API server. And this is very important, because
this
is the special password that no one should ever have access to. And so we take all of these
three things, we pass it into a hashing function, essentially, and then we create a
signature.
And so we take these three things, and that's going to make up our token. And so we send that
back to the user. Now, let's say that we've got a user who decides that he wants to do a little bit of
shady
things. And he wants to kind of hack our application and do some bad things to our
application
ultimately. And so what he's figured out is like, hey, look, this token, right, it's not
encrypted,
I can see all the data, I can see that I'm a user. And so what he's decided to do, he changed
a few bits in the token to make it now read admin instead of user. But he could have really
done
anything, right, he could have changed the ID so that it's someone else's ID, and he could
potentially have access to someone else's information, it doesn't really matter, but
he's changed a few things. And so now he's got a this token, which has the header signature
and
payload. And he's now a admin user, or the payload says he's an admin user. Now, the reason he
can't
actually do this is because ultimately, remember, the signature that's currently in the token
was
generated by having a header, the same header, and then you pass in the payload with this
specific
ID and a role of user. And then you pass in the secret. And so this signature is no longer
valid,
he can't use this same signature, because it's not going to match up with the data that he
has. So if we actually try to create a signature from this, it's going to look different. And so
what
he has to do is he has to create a brand new signature so that it matches the data he's
sending. However, he can't create a brand new signature, because he doesn't have this super secret
special
password. This only resides on our API server. And so if he tries to create a brand new
signature,
he can only pass in the header and this payload into the hashing function to generate a new
signature. And so that once again, is not going to be a valid signature. Because when he
sends,
let's say he does create that signature with only the header and the payload, and no secret,
what we now have this token, right, that's made up of these three things, we send it to our
API.
And what our API is going to do is to verify if the token is valid, he does a super simple
test.
He takes the header, he takes the payload, and he takes the secret. And he creates a test
signature.
Because remember, that's what we did in the first place to actually create the original token,
we take the header, the payload, the secret, and we pass it in to our signing function. And we
pass
into our hashing function and create the signature. So we do the same thing. And then we take
the signature in the token that was sent by the user. And we just compare them, if they don't
match,
which they won't, because remember, he doesn't have access to the secret. And since we use the
secret in our hashing function, right, the signatures aren't going to match. And we
know
that this token is not valid. So this signature once again, only ensures that the data
integrity
is still valid, that no one has tweaked any of the bits, no one's changed any of the data,
that's all the signature can do, it can just verify that it hasn't been changed.
Since we created the token in the first place. But once again, and I can't stress this
enough,
anybody can change the data of a token, anybody can see the data of the token, they just
can't
generate a brand new signature, because they don't have access to the secret. And so that's
why the password is so important, because that's how we ensure that the token hasn't been manipulated
or
changed in any way, shape or form. So hopefully that made sense. I didn't understand this
at
first, it took me a while to actually grasp why we needed the signature. I just kind of was
like, I'll figure this out later. But after a year or so, I was like, Oh, this is actually a super
simple
concept. And hopefully I can do a slightly better job of explaining it than some of the
documents that I read when I was first learning it. And hopefully this kind of, you know, gets rid of
some
of the unnecessary stress that kind of comes with learning about JWT based
authentication.
So once again, let's quickly take a look at how we're going to handle logging in the user,
more specifically, how do we actually verify that his credentials are correct, because it's not
going
to be exactly the same way that you would think. So the user is going to hit the login
endpoint,
and he's going to provide the email and the password. And I've colored the word password in
red, because this password is going to represent what the user is trying to log in as this is
his
attempted password, that's what we're going to call it. And that's going to be in red. And
keep in mind that when he sends this password, it's in plain text, right, it's just the regular
password
that he typed out. And so when it gets to our API, the first thing that we're going to do is
we're going to hit our database to try to find the user based off his email or his username. And
the
database is going to send back all of the information about that user, which includes the
password. However, if you remember how we actually created the password and stored it in the
database,
we actually stored a hashed version of the password in the database so that if anyone hacks
our database, the user's passwords aren't actually in plain text, so no one can really hack
their
passwords. But this creates a little bit of a problem because we have the attempted
password,
the for the login attempt in plain text, however, we've got the password in our database as a
hashed
password. So how exactly do we check to see that they're equal? Because right now, a hashed
password
does not equal the same as the regular password. And your first instinct is to, hey, well,
let's
just convert this back to a plain text password. And we can't exactly do that. Because
remember, a hash is only one way. So if we hash a password, we can't get the original password from the
hash,
that's what's great about hashing a password. And if you think you can do that, that's
actually, well, I guess what we would refer to as encryption, but we're hashing in this case. So what do we
do?
To verify if the passwords are actually equal, it's pretty simple, we take the hashed
password,
and then we take the raw. And once again, this hashed password is the correct password in our
database, then we take the password attempt, and we hash it again. So we hash this password. And
so
then we get a hashed password. And so if we have the correct password, then if we put it
through
the hashing function, these two should be equal. And so if they're equal, that means the
password they provided us is the correct password in the database. If it's not the if they don't
match,
that means he gave us the wrong password. And so if they're correct, we'll then go ahead and
send
we'll create a token first, and then we'll send it to the client. So it's as simple as taking
the attempted password, hashing it, and then comparing it with the hashed password in the
database.
Because if the passwords are correct, then the hash should equal the hash in the database.
Okay, so we're going to create the login path operation. And what I'm actually going to do
is
you're probably thinking that we can just store the path operation in the users.py router.
However,
I think it would make more sense to actually store this in a authentication router. So we're
actually going to create a brand new router for this so that we can keep the user routes
versus
the authentication routes in two different files. However, if you really wanted to, you could
store it all in one file. I'm gonna call this auth.py. And the first thing that we have to do is
from
the fast API, we're going to import a few things. We're going to import the API router. We're
going
to need depends status, HTTP exception. And maybe we need the response I haven't decided. And
then
we're going to define our router. So we'll say router equals API router. And we're going to
pass
in a tag, you know, just for the documentation. So we'll say tags equals authentication. All
right,
and then we're going to create our login. So we'll call this well, first of all, this is going
to be
a POST request. Because remember, the user is going to have to provide his credentials. So
generally, when you want to send data in one direction, it's going to be a POST
request.
And we'll send it to the login endpoint, you can call this whatever you want, you can call it
authenticate. However, I like login. And then we'll define our function. And I'll just call this
login
as well. And because we're ultimately going to have to fetch the user from the database,
we're
going to have to import our our database session. And so from SQL alchemy, we'll say dot or m
import
session. And so we'll do the same thing that we did with every other path operation, I'll say
DB
session equals depends. And then we're going to have to actually import our get DB function.
So
from here, we'll do from and we have to go up a a directory and I'll say database import
session.
Sorry, not session. Actually, you know what I'm gonna do, I'm just gonna say from dot dot
import
database. And then I can just reference it as database dot get underscore DB. And since
the
user is going to be providing the login information, I think it makes sense to set up a schema
so that
we can ensure that they provide the exact pieces of data that we ultimately want. And what
we're
going to do is we're just going to create a new class. And I'm going to call this user login.
And this will extend the base model as usual. And the two pieces of information that we want
our
email, which is gonna be a string and then we can do password, which is going to be a
string.
Actually, I'm gonna I'm going to use an email string like we did before. And then from
our
auth.py, I'm also going to import schemas. And here, I'm going to store the schema as a
user
credentials. There's gonna be schemas dot user login. Alright, and so now we can access all
the
user information or the attempted login information with the user credentials variable. And
we're going to make a request to our database, specifically, our users table to retrieve
the
user based off of his email. So we'll say db dot query. And we'll and we have to import models
now
so that we can access it. We'll say models dot user. And then we have to filter based off of
the
email. I'll say models dot user once again, dot email equals equals user underscore
credentials
dot email. And then there's only going to be one user with that specific email. So we'll do
first
and we'll store this result in a variable called user. Now, if there's no user
with
that specific email, then we're going to return a error or we're going to raise an exception.
So we'll say if not user, we're going to raise an HTTP exception. And the status code is going
to
be status dot and I'm trying to remember what code was up, it's going to be a 404 because that
user
doesn't exist. And then the detail field, we're just going to say invalid credentials.
Because
normally with authentication, you don't want to say, hey, this is the wrong email, or this is
the wrong password, because you don't want to make it a little bit easier for them to kind of guess
other
people's information, just say this is invalid credentials, let them figure out whether it's
the email or the password. And then after this, once we verify that we did actually get a
user
object, we have to verify that the passwords are equal. So this would be, you know, performing
that logic of, of hashing the password that they gave us and seeing if that it compares to
the
password from the database. And so what we're going to do is we're going to create a new
function in the utils folder, that's going to be responsible for comparing the two hashes, or actually,
it's
going to take in the raw password, the password attempt, it's going to hash it for us, and
then it's going to compare it to the hash in the database. So we'll create a function, we'll
call
it a verify. And it's going to include the plain password, which is the password the user is
trying to attempt, and then the hash password, which comes from the database. And all we have to
do
is just return. And then we can reference the PW context, right? So if you want to hash a
password,
you just call the dash, the dot hash method. However, it's also got a another method for
us,
which is verify. So it's going to perform all this logic for us. So we just pass in the plain
password. And then we pass in the hash password. And it's going to figure out the rest.
Now,
you're thinking, well, you know, what is the point of such a tiny function? Why don't we just
include it in our auth.py? Well, we 100% could because, you know, it's, it's really two
lines
of code, it doesn't really make sense to store it in another file. However, then we also have
to import all of the the bcrypt code. And I like to have everything kind of separate. So keeping
all
of the bcrypt logic in one file just makes things a little bit easier to manage. But in our
auth folder, we can import utils now. And we're going to run the utils dot verify. And we're going
to
pass in the first the plain password, which is going to be stored in user credentials. So
we'll
say user credentials, dot password. And then we pass in the hashed password from the
database,
which is stored under users, we'll say user dot password. Alright, and if they are not
equal,
so if they do not equal each other, which means they provided us the incorrect password, then
we're going to raise another HTTP exception. And this is going to be the same thing,
we're
going to send a status code of 404. And the detail is going to be the same thing, we're not
going to
tell them that it was the wrong password, we're just going to say invalid credentials, so that
it's not easy for them to kind of keep guessing passwords or emails. Alright, and then at
this
point, what we would do is we would create a token, which, you know, we haven't really gone
over how
to do that. And then we would just return return token. But once again, we haven't done that.
So
I'm just going to return. And we'll just say token. And I'm just gonna say example
token,
right, because we haven't implemented that logic. But I do want to make sure that the rest of
the code looks good. So we'll try that out. And I'm going to go in, create a new
request.
This will be login user will be a post request. Copy this. Actually, this is just gonna be
slash
login. And then in our body, we're going to provide some JSON. And here, we're just gonna say
email
is whatever. And password is whatever. And before I actually do anything, I'm going to run a
query
on my database. Because if you see, I've got a couple of users that don't have hashed
passwords.
So I'm actually going to delete everything, just so I can clean up some mess. And I'll just
say,
and what I'm actually going to do is I'm just going to delete everything. I think that's going
to just make things simple, because I don't even remember these guys as passwords. So it's
going
to create some issues for us. So we'll just say delete from and I'll say users. And then I
just won't provide any condition. So this should delete everything. We'll run that. And you can see
that
it looks like it deleted all 12 results. And that's perfectly okay. And I'll add my
original
query back select star from users. And we're going to do is we're going to just register new
users.
So I do create user. And I'm going to call this Sanjeev at Gmail, and then the password is
going
to be password 123. Let's create that user. Let's take a look. All right, we've created that
user.
And now let's log in this user. So we'll go to login, and I'm going to provide my Sanjeev
at
Gmail. And then we're going to provide a password, which was password 123. Let's try this
out.
And it looks like we ran into an issue. So what? Well, first of all, that's the wrong error
message.
I'm not sure why it gave me that. Let's just it should say invalid credentials. So
where
is this actually happening? And guys, I made an absolute silly and stupid mistake. I forgot
to
actually wire up this router in our main.py file. So it doesn't even know about it. So let's
import
this. We're going to say import auth. And all we have to do is just one skin. And I'm just
going
to copy this. And we'll just say auth dot router. Alright, let's try this again. Right. And
we've
got our example token, which means that we verified that the passwords are correct, and
everything was
good to go. We just haven't implemented the logic for the token. But let's just make sure that
when we put in the wrong password, it still works. Or actually, it shouldn't work. And we get
invalid
credentials, let's put the right password back in. And then let's use the wrong email. And we
get
invalid credentials as well. So, so far, we are almost done with the whole login process. The
next
thing that we got to do is handle creating a token, it's not going to be too difficult. But I
do want
to save that for the next video. Now, if we head on over to the fast API documentation under
security,
there's going to be a walk with password. And so most of the things I cover are going to be
coming from this documentation right here. Now, the first thing that we have to do is we have to install
a
library that handles signing and verifying JWT tokens. And so we're going to use this Python
dash
and shows the library, and then we have to provide a cryptography back, back end. So we're
just going
to copy this line right here. And we're going to run this in the command line. And what I'm
going
to do is when it comes to authentication, and anything with JWT tokens, I'm going to
create
a new file. And I'm gonna call this OAuth two dot p y. Alright, and the first thing that
we're
going to do is we're going to import from Josie. We're going to import JWT error, and JWT.
Now,
there's going to be three things, three pieces of information for our token. Well, actually,
well, there's three pieces of information. But there's three other things about the token
that
we need to provide. So we're going to say, we're going to need the secret key, right? That's
that special key that I mentioned that ultimately handles verifying the data integrity of our
token,
which resides on our server only. So we're gonna have to provide that secret key. We're also
going to need to provide the algorithm that we want to use. We're going to be using HS 256. And
then
we're going to need to provide one other thing, which is the expiration time of the token. So
we
haven't really discussed the expiration time, if we just give a plain token, without an
expiration date, that means that users logged in forever. And there's no application that just lets a
user
log in forever, I don't think. So we're going to provide a expiration time so that we can
dictate
exactly how long a user should be logged in after they actually perform a login operation. And
so
we need an expiration time. And what I'm going to do is I'm going to just save these as
variables,
I'm going to say secret key equals and then this is just going to be some arbitrary long text.
I'm
just going to paste that in here. And if you're just wondering why, I mean, this, if you just
follow the documentation, actually, you'll see that they do the same thing. So we just need
to
give it some really long text in this case. And it even gives you a command to kind of get a
string
like that for your password. Because you you could theoretically just put, you know,
hello,
or something here. And that's going to work just fine. However, it's not quite as secure. But
for learning purposes, it doesn't matter, just provide some kind of string, right? And then the
algorithm
is going to look like this. And then the expiration time is going to look like this. So I'm
just going to copy this from the documentation. And then we're going to define our function. So this is
going to
be create access token. And what we're going to do is remember, the access token is going to
have a
payload. So whatever data we want to encode into the token, we have to provide that. So I'm
going to pass that in as a variable called data. And this is going to be of type dict. And what
we're
going to do is we're actually going to make a copy of this data, because I don't want to
actually change it. I want to make a copy of it, because we're gonna, we're going to manipulate a few
things.
And I don't want to accidentally change the original data. So we're going to say data dot
copy. So this is going to make a copy and I'm going to store it in a new variable
is going to be to underscore code. So this is all the data that we're going to encode into our
JWT token. And then now we're going to create the expiration field. So to
actually do that, there's a couple of things. First of all, right now we have it set to 30
minutes. And so what we need to do is to provide the time of 30 minutes from now, right. So we have to
provide
the time that it's going to expire in. So we have to grab the current time and then add 30
minutes.
And so anytime you're working with dates and times, we have to import the date time library.
We'll say date time, import date, time and time delta. And I'm going to say expire
equals
date time dot now. So this is going to grab the current time. And I'm going to pass in time
delta
of and then since this is in minutes, I would say minutes equals access token expire
minutes.
And then what we want to do is we want to grab the to encode, which is a copy of a dictionary.
So this is also a dictionary.
And I want to update it. And here, we're going to pass in
expiration. And then we're going to provide the expire time. So we're just adding that extra
property into the into all of that data that we want to encode into our JWT. And so now our
JWT
will tell us when it's going to expire. And what we're going to do is we're going to call
JWT
coming from the is it Jose, I guess it's at the Jose library. So JWT dot encode that this
method
is actually going to create the JWT token. And we'll say the first property is everything
that
we want to put into the payload. The second one is going to be the secret key at the
signature.
And then we have to specify the algorithm. The algorithm equals algorithm algorithm right
there.
And at this point, we're just going to return all we have to save this in a variable. And
here, we just do a coded JWT. And then now we can go back to our specific path
operation.
And since we're already importing, actually, we have to also import OAuth two. And then here,
we're going to create an access token. And we're going to call the
sorry, not utils OAuth two dot create access token. I'm going to say the data equals and then
we're
going to pass in a dictionary. So here, the user ID. Remember, this is the data that we want
to
put in the payload. So I have decided that I'm going to put in the user ID. And pretty
much
nothing else. I don't really care about anything else. We could give it a role. We can do
something else. But for me, I just want to encode the user ID. So that's what I'm passing in
here.
However, if you wanted to provide some extra information about the user, if you wanted to
provide, you know, the scope of different endpoints, they can access, you can put all of
that
information in here. And so I'm gonna say the user ID is going to be set to user dot ID. And
so now
what we can do is we're going to return a few things. I'm gonna say we're going to return the
access token, which equals access token. And then we're going to tell the user what kind of
token
this is. So this is token underscore type. And this is what's referred to as a bearer token.
And
I'll explain how to actually configure that on the front end. But literally in the
authorization
header, we just write the word bearer, and then we provide the token. So nothing special
there. All right, let's try that out. And it looks like I got an error. I don't know why I put an
equal
sign there. And let's try this out. So we've got we've got the login endpoint, let's put in
the
correct email this time. And let's see what happens. Look at that, we got an access
token.
And this kind of looks like a JWT token, right? It just looks like a bunch of random text. And
then it tells us it's a bearer token. So let's actually copy this. And what I want you guys
to
do is go to your web browser. And I want you to search for JWT. And then go to the first
one,
go to JWT.io. And then here, what we can do is we can paste in our JWT token. And what's
really
cool is it's going to decode the token for us. And so you can see this is the algorithm we
used, it is a JWT token. But take a look at the user ID. Right, this is the actual user ID that we
passed
into the token. So all of the data that we encrypted into the token are all sitting right
here. So we also got the expiration time. And then the signature somewhere in here as
well.
But isn't that pretty cool, right? It was able to decode that. And it's important to
understand that, you know, none of this is encrypted, anybody can see this information. But by being able
to
sign it, we know that no one can kind of mess with it. And we can also specify an expiration
time. So we know that how long this token is valid. So when the our API gets it, it's going to
just
verify that nobody touched the token, it's going to verify that, hey, the expiration time, you
know, isn't before the current time, which would mean that it's already expired. At that point,
it
already knows the tokens valid. And then we can assume that everything is good to go. We're
going
to make one small change when it comes to retrieving the user's credentials in our
login
route, instead of passing it in the body, we're going to use a built in utility in the fast
API library. So if we do from fast API dot security dot oh, whoops, OAuth two, we're going to
import
something called OAuth to password request form. And so what we can actually do is instead of
just
doing the the usual here with the user credentials, we're actually going to delete that. And
I'm going
to provide a dependency. Well, actually, I need the user credentials because we have to store
it someplace. But here we say, this equals OAuth to password request form equals depends. So
we're
setting up a dependency kind of like we do with the database. And so this is going to require
us to retrieve the credentials. And then fast API is automatically going to store it in side
this
variable called user credentials. However, we have to make one small change. So the username
in when
you retrieve the the user's attempted credentials from here, what it's going to do is it's
going to
store it in a field not called email, but it's going to store it in a field called username.
So when we compare the models dot user at email, when we're querying the database,
we can't compare to user underscore credentials that email because there's no field called
that email, right, it's going to only return two things, it's going to return. Oops, it's
going
to return something, it's going to return our username, which equals whatever, and then it's
going to return our password, which equals whatever. And so we don't have access to
email,
we have to use user dot user underscore credentials dot username,
because,
well, this is actually a bad example, because you have to think of it as like a dictionary,
right, it's coming in, like this, and then there's going to be a, a user field.
You're gonna have username, or why is that capitalized username, which
equals
well, you're going to have username, which equals, you know, blah, and then you're going to
have
password, which equals blah, right. And so we'll just tag user credentials dot and then
we'll
grab the username, which in our case will happen to be the email, the OAuth to password
request
form doesn't really care what, what the username is, it could be a username, it can be a
email,
it can be an ID, it doesn't really matter, it doesn't really care, it's just whatever the user
actually sends, it's just going to store it in a field called username. So those are all
of
the changes that we have to make from our back end side. Now, when it comes to testing
things,
we no longer send the credentials in the body, like we normally do. Like if I try to send this
now, we're going to get an error, right, because it says, username is field required value
error
missing. So what we and the password is also missing. So what it's doing is it no longer
expects it here. Instead, it expects it inside form data. So here, I'm going to say username. And this
is
going to be my email in this case. And then my password here. And so let's try this now. And
now
it successfully works. So those were the couple of changes that we have to make. But you'll
see that it makes life a little bit easier by setting up that dependency and using the built in
functionality
of fast API. In the last lesson, we learned how to log in a user by sending a request to the
login
endpoint and providing the username and password. And our API will then return an access
token, which the user can then use to retrieve data from our API. So anytime he needs to access a
endpoint
or path operation that requires a user to be login, he'll just send this JWT token in
the
payload. And then our API has to actually validate the token. So in this video, we're going to
handle
the logic for verifying that the token is still valid, and that they didn't tamper with it,
as
well as verifying that the token hasn't expired. Now, before we do anything, what we're
actually
going to do is we're going to define a schema for the token. Because we know that the user has
to
provide the access token. So just like any other piece of data, if we expect them to send
something,
it's best to set up a schema. So we're just going to set up a schema for access token and
token type and just make sure that they match accordingly. And so here we're going to do class token.
And
we'll say the access token is going to be of type string. And the token underscore type is
going to
be of type string as well. And then we can also set up a a schema for the token data so that
the
data that we embedded into our access token, so we can say token data base model. And then
here,
we did embed the ID. But I'm going to say this is optional for now. And it's gonna be a type
of
string if it is set. So it can be optional. And we got to import optional as well. And so
that's
going to come from the typing library. And then in our OAuth two file, we created an access
token
function. And then we have to create a function to verify the access token. So let's create
the function and it's going to just be called verify access token. And what we're going to do is
we're
going to pass in a token, which is going to be a string. And we're also going to pass in
the
specific credential exception. So we're going to pass in what our exception should be if
the
credentials don't match, or if there's some issue with the token. So we'll just store this in
a variable called credentials underscore exception. And I'll explain this a little bit later. And
so
here, I'm going to say, JWT, right, so we're going to access the JWT library, and we'll say
JWT dot.
And it's going to have a function. And if you take a look at our options, I'm sure you have an
idea because creating a token we did, I encode dot encode, I'm guessing you can figure out what
is
the specific method for decoding, it's going to be decode, obviously.
And here, we're going to pass in a couple things. First of all, the token, then we have to
pass in the secret key, so that we can decode it.
And then we have to pass in the algorithm that's used that we can just pass an algorithm.
Once
again, these both of these are coming from these variables right here. Not it's obviously not
a good idea to store the secret key within your actual code. But like I said, we're going to
have
a later section where we'll turn these into environment variables so that it's not hard coded
into our code. And we're going to store this in payload in a variable called payload.
Alright, and so this will just store all of our payload data. And to extract the data, what we
can do is we can say payload dot get, and then we have to get the specific field that we put
in.
So if I go back to my auth.py file in my routers, and you take a look at my data, you can see
that
we have a field called user underscore ID. And that's going to get the ID of the user. So
here, we just say, get. And then we just pass in that same exact name users underscore ID. And
we'll
say that this is going to be stored in a variable called ID. And this should be of type
string.
And if there's no ID, then we're going to raise a credentials exception. Right? So
whatever
exception we provided into this function, it's going to raise that. And then we're going to
say, token underscore data equals schemas dot token data with the ID equals the ID that we
extracted
out of here. And I see that we didn't import schema. So I'll import that real quick. So
we'll
say from dot import schemas. And so all this is going to do is is just going to validate, you
know,
that it matches our specific token schema. Now, if you look at our schema, right, it's
literally one thing. So it's not super exciting. And I made it optional. So we shouldn't actually make
it
optional. But we'll come back to fixing this in a bit. But this is just going to ensure that
all the data we pass in the token is actually there. And so that's why I'm using a schema for
that.
However, for one actual variable, you don't actually need to do that, especially since we're
checking to see if it exists right here. But it's good to always make sure so in the
future,
if we do add extra fields, we can also validate the schema here. Now, we're almost done with
this
function. However, there's one little issue, because we can run into an error in any one
of
these lines. And so anytime you're working with code that can error out, you want to do a try
accept block. So we'll do try. And I'm going to indent these. And then here, we'll say
accept.
And then we're going to pass JWT error. And this should not be capitalized. And
remember,
this is coming from the Jose library. And once again, we're just going to raise a
credentials
exception, if there's any kind of error that we didn't account for. And the next thing that
we
have to do is define one last function. And this is going to be called get current user. So
what
this is going to do is, and so what this is ultimately going to do is that we can pass this as
a dependency into any one of our path operations. And when we do that, what it's going to do
is
it's going to take the token from the request automatically extract the ID for us, it's
going
to well, it's going to verify that the token is correct by calling the verify access token.
And then it's going to extract the ID. And then if we want to, we can have it automatically
fetch
the user from the database and then add it into as a parameter into our path operation
function.
So here, all we're going to do is we're going to pass the token. And so this is going to be a
type
string. And then here, we just say depends, which has to be imported from the fast API
library.
So we'll say from fast API import, we're going to import depends status and HTTP
exception.
So here, we'll say depends, and then we'll pass in a OAuth two schema, or OAuth two
scheme.
So what we have to do here is it's gonna be a little confusing. I'll say, OAuth two
underscore
scheme. And this is going to be equal to and we have to import one more thing. So we'll
say,
from fast API dot security, import OAuth two password bearer. And then here, we just
reference
OAuth two password bearer. And what we have to do is we have to provide one field called token
URL.
And so this is going to be the the endpoint of our basically our login endpoint. And so if you
go
to your auth.py, you just grabbed whatever name this is, keep in mind, you don't have to name
it login, but you just have to pass this into here. Actually, sorry, you remove the slash, so
it's
just login. And then we're going to grab this variable, and I'm just going to pass it into
here.
And this is kind of just tying everything together.
And then we have to define our credentials exception that we're going to pass into the verify
access token function. So when the credentials wrong, or there's some kind of
issue with the JWT token, what exception should we raise? So here, I'm just going to say HTTP
exception. And we'll set the status code to be a 401. So unauthorized detail here, I'm going
to
pass a string once again, could not validate credentials. And then we have to set
some
headers as well. And so just go ahead and just copy this in. And then finally, we're going
to
return a call to our verify access token function. And since we have the token passing to the
get
a current user, we can pass it into our verify access token. And then we also can provide
the
credentials. All right, so just to quickly recap, because I know we did a lot, and I think
some of
it may be confusing. But what's going to happen is anytime we have a specific endpoint that
should be
protected, and what that means is that the user needs to be logged in to use it. What we're
going to do is I'm going to, well, as an example, let's say that users who want to be able to create
a
post, they need to be logged in, what we can do is we can just add in an extra dependency into
the path operation function. So I can say, here, I would just say get current underscore
user,
which would return an int. And then we'll say this equals and then we pass in a dependency. So
we'll
say depends on a lot to get underscore current user. So this is going to add a
dependency,
which is going to be that function that we created called get current user. So anytime anyone
wants to access a resource that requires them to be logged in, we're going to expect that they
provide
an access token. And then we provide this dependency, which is going to call this
function
get current, sorry, where is it get current user, and then we pass in the token that comes
from the
request, we're going to then run this verify access token, in this case, and then it's going
to
provide all of the logic for verifying that the token is okay, and that there's no errors.
And
if there's no errors, then we go ahead and return nothing essentially. And that means that
they were
successfully able to be authenticated, if we do return some kind of error, with the
credentials
exception, then they're going to get that appropriate 401 response back. So that's how our
login works. It's nothing special. But there are a couple of different components that are
involved.
And in the next lecture, we'll start to take a look at starting to protect our specific
endpoints
so that it forces users to be logged in to actually perform that operation. And guys, I made
one little mistake. In the verify access token function, I forgot to return something.
And so if you ran into any issues, it's because of this. So what we're going to do is here is
we're
just going to do return token underscore data. And so what's really happening here is once
again,
we're going to decode the JWT, we're going to extract the ID, if there's no ID, then
we're
going to throw an error. And then we're going to validate with a schema, the actual token
data, which in this case is just an ID. So that's the only field. But if you had extra properties
or
extra information, you can pass that in. And then we want to make sure we return the token
data so that we can actually make use of that data. And keep in mind, remember, the get current user
is
what actually calls the verify access token. So when it calls verify access token, it expects
us to return the token data. And then when we get the token data, we return it to whoever calls
this
function. Okay, guys, so I found a few extra bugs that we need to fix. Now, the first thing
that I
messed up was when we actually set the expiration time under the create access token
function,
instead of datetime.now, it should be datetime. UTC. Now, this is important. Because as I was
testing
it, I kept getting an expiration error. And that's because this should be UTC. Now, the second
thing
is, we want to put this in brackets, because I guess I expect a list of algorithms
maybe,
we'll put that in there. And then finally, this is also a bug right here. Because if you take
a look at the payload, when we create the token, which I believe should be in the auth.py,
when
we log in, we pass in the data as user underscore ID, and not users underscore ID, like we did
here.
So remove that. And this should prevent us from running into any other potential issues in
the
upcoming lectures. Sorry about that. I know we ran into a couple of bugs. But that's what
happens when you try and copy and paste really quickly. Before we wrap things up, there's one last
change
that I want to make inside the login route in the auth.py router file. I noticed that for
the
response or the exception that we raise if the either the user's email is wrong, or if
their,
if their password is incorrect. I use the wrong HTTP status code for the exception. And I
can't
remember what I actually had it before, because I went ahead and fixed it. But I want you guys
to go ahead and change the two exceptions here. In both of those cases to a 403. I think that's
a
better representation of what we should be sending when the user doesn't provide proper
credentials.
So just update it here and then update it here as well. And then you guys should be good to
go.
Okay, guys, we're pretty much done with all of the authentication side of things. The only
thing that we have to do is require the user to authenticate. Because right now, you
know,
if we perform any operation like creating a post, you can see that I could just create a
post.
And that's it, I don't have to log in first, I don't have to do anything. So anyone can create
posts, anyone can delete posts, anyone can do anything they want. Obviously, that's not
how
your API is going to work. You're going to want to ensure that users are logged in to perform
certain operations. And there may be certain operations where they don't necessarily need
to
be logged in depending on how you want to structure your application. Because you know, if
it's like a Twitter like application, right, anyone can see anyone's tweets,
I just can't delete anyone else's tweets, right. And to create a tweet, I have to be logged
in, and so it depends on what you want to do for your application. But what we're going to do
is,
we're going to start off by forcing the user to be logged in before they can create a post.
And doing this is actually really simple. So let's go to our post a py. And let's find our
create
posts. Now, the first thing I want to do is I'm going to import a lot to which is coming from
this
OAuth to py file. And then in our create posts, path operation function, we're going to add
an
extra dependency. And this dependency is going to be the create or sorry, get current
user
function that we defined in our OAuth to file. So we'll say is depends. And then we'll say
OAuth
to dot get underscore current user. And then we're going to store this in a variable called
we'll say
user underscore ID. And this is going to be a integer. Okay, and so all this is saying is
that
this function is now going to be a dependency. So this is what forces the users to have to be
logged
in before they can actually create a post. And so when this function is called, whenever they
hit this endpoint, the first thing that we're going to do is we're going to call this function.
And
this function, all this function does is really just call the verify access token, but and
passing in the token, the token which comes from the user. And so it takes the token, we first decode
the
token, we extract the ID from the payload. And if there's no ID, we throw an error. And then
you can
see we validate the schema. And then this site, this is something I actually added in off
camera.
So we can just delete that, and then go back to what you guys have. But we ultimately return
the
token data, which is nothing more than the ID, right? So we could rename this as ID for now.
But like I said, in the future, you may want to add extra fields into the payload, and then it's
no
longer just the ID, it's going to include extra information. So we're going to return the ID,
which then gets returned by the get current user function. And then in our post up py,
in our function, we're going to return the ID and store it in a variable called user ID. And
so then we can ultimately access the user ID by just calling user underscore ID. That's
it.
And we can do whatever we want with this user ID. And you'll see, we'll eventually add some
more logic. But for now, I'm just going to print it out just to see what we see. And let's go
ahead
and try this. So first of all, I'm going to create post. And then we're going to see what
happens now
when I try to create a post. Look at that not authenticated. So by putting in that extra
dependency, we have ensured that the user has to be authenticated before they can use this
post.
Now, how do we actually provide the token so that we can actually use the create post?
Well,
first of all, let's get a token. So we're going to go to the login user, I'm going to hit
send, and get a brand new token. And then what we want to do is we want to go to create post. And
then
we want to go do headers. And then we want to create a header. And you know, you can see I
can
I created one already. But I'm going to type this out for you from scratch in the line below
it. So we'd say authorization is going to be the key. And then the column is going to be and you
type
the word in bear because it's a bear type token. So you do bear with a capital B space, don't
forget
the space, and then paste it in. Okay, and so now this is included in our header. And so we
should
be able to send a request. And so now look at this, we were now successfully able to send
our
token, the API was able to validate it was a valid token. And it was able to then allow us to
create
a post. And just another little postman tip, you can uncheck this for now so that it's
not
authorized for you. If I try to do it now, you see I get an error, you can go into
authorization and then just type in go to bear token and then paste it into here does the same exact
thing.
It just you don't have to type in the word bear for yourself. And then you know, have to do
all of that, you can just hit send now. And it does does the same exact thing, whichever method
you
prefer more. Now that we've protected our create posts route, let's go ahead and do this with
some
of our other routes. So it's just a matter of just copying the dependency for get current
user. And then for retrieving a post, well, remember, this part is up to you, you decide ultimately
what
routes you want a user to be logged in to actually perform an operation. But for delete
posts,
I'm definitely going to force a user to be logged in. And for update post, I'm going to do the
same thing. And then finally, you know, like I said, forgetting posts, you know, it's really up to
you,
we'll say that you have to be logged in to do anything. So forget posts are getting all posts,
we want to make sure that they're logged in. And then forgetting an individual post, we want
to
make sure that they're logged in. And then finally, the last thing that we well, it's not the
last
thing. But one of the things I forgot to do is under auth.py, when we send our access
token,
after they log in, if you remember, in under schemas, we actually created a token
model,
sorry, token schema, we never actually used it. So let's actually use that by setting the
response
model here. And this is going to be schema dot token, he must dot token. Well, let's save
this.
And this should be capitalized. Sorry about that. And so now if we log in a user, we should
still get no errors. So perfect. Okay, so just in case
in the future, we accidentally change something, we're still going to perform that validation
to ensure that the token we only send those two fields when we return a token. Now let's
quickly
just test all the other routes. So if I do get posts, and I hit send, you can see, well, let
me
save everything. Sorry about that. Once again. Now if I hit send, you can see I'm not
authenticated.
So I have to do the same thing. So once again, you can do authorization bearer, or you can
go
to authorization, and then just select bearer token and then paste in the token. Now if I
hit
send, oh, sorry, I got to recopy the token again. Now if we send, it works now get one post,
let's
try this not authenticated, we'll go into authorization. Bear token. Send that
works
deleting posts. Go to once again to authorization. And there's no post with an idea of
three,
about four. All right, that worked. And then finally, update post. We're going to do the
last
thing and bear token. All right, and that works. And then let's just double check to make sure
that
if there's no token, what happens? Error, perfect. Okay, guys, so I think that's going to wrap
up this
video. For now, we've pretty much done all the authentication that we need to do up to this
point. There's one last thing that I want to test to verify that everything works the way it
should.
And we probably should test this in one of the previous videos, but I forgot to do it. And I
want to make sure that we test the expiration process because a JWT has an expiration time that we
set
ourselves. And this just ensures that a user is logged in only for a certain amount of time.
Now
we set the access token expiration time to be 30 minutes. And so that means that if a user
logs in
and he keeps his token for more than 30 minutes, after that 30 minute mark, if he tries to use
that token to access any of our endpoints, it should throw an error because it's already
expired.
So let's test this out. And the easiest way to test this is first of all, we're not going to
wait 30 minutes, that's ridiculous. So what we're going to do is, we're going to set this to
one
minute. And we're just going to test this real quick. And so I'm going to log in this
user.
So this token is valid for exactly one minute. And so we'll go to create posts, actually,
we'll just
do get posts, it doesn't really matter. And I'll paste this in here. And you'll see this
works. And we're going to just wait for one full minute. And after one minute, let's just verify that
we
get a unauthorized error because the token has expired. Okay, so it should be about one
minute
now. And so if we test this, we should now get an error. So after one minute, it says it could
not
validate credentials. Now we could set up a log message to say that, hey, this token is
expired, but we don't need to worry too much about that. As long as it throws an error, and it gives
them
a 401. I think that's good enough for now. And so this confirms that our
expiration
functionality works. And the last thing to do is, well, let's make sure we change this back.
So once
again, it doesn't matter what time you choose, it's up to you, I'm just going to do 30
minutes, actually, I'm going to do 60 minutes just for testing purposes. Moving forward, I don't
want
to have to continually get a new token after every 30 minutes. So 60 minutes is a good
number.
Now you might be wondering, why exactly do we have this get current user
function,
when all it really does is just call verify access token, we could completely just remove this
and
just call this directly whenever we want to authenticate a user. And you absolutely
could
with the current implementation. But the idea behind the get current user function is
that
once the verify access token returns the token data, which is the ID, the get current
user
function should actually fetch the user from the database. And so that way, we can attach the
user
to any path operation, and then we can perform any necessary logic. Now, you don't actually
have to fetch the user here. It's up to you how you want to implement this, if you want each of your
path
operations to fetch the user themselves, they have the ID, so they can do it themselves.
However, if you wanted to automatically do it here, you 100% can do that. And so I'm going to show you
guys
how you can do that in this lesson. And so keep in mind, we get the token data back, which is
going
to be nothing more than an ID. And what we're going to do is first of all, we need access
to
our database so that we can fetch the users. So from here, I'm going to import database. And
within
this function, we can pass in the other dependencies so that we can actually get access to the
DB
object. So I'll say depends, and then we're going to access database dot, and I already forgot
the
name of the function, what does it get underscore DB, get underscore DB, and we'll say DB
session
equals and then we have to import session from SQL alchemy. And so now we can make requests
to
a database. And what we're going to do here is first of all, I'm no longer going to return
that
directly. And I'm going to say token equals and then we're going to call that this function
right
here, verify access token. All right, and since we have access to the token, what we can do
now is we
can say DB dot query. And then we have to import models. And I'm not sure if we've done
that
already, we haven't. So I'll import models. I'll say models dot user dot filter. And then we
want
to filter based off of the ID. So I'll say models dot user dot ID equals equals token. And
then we
just grab the ID field. And then we're going to grab the first one. And then we can return
the
user, which I forgot to save it. So user is going to be equal to the result of that. And if
you want
to, you can print user here. But I already know this is going to work. So we can just go and
save
this. And then in our post at py, right, when we call this dependency right here, it's no
longer
returning the user ID. So I don't think it's it's a good practice to call this user ID
anymore, because it represents a user. So I'm going to say, I'm gonna call this current underscore user.
And
we're going to rename this everywhere. All right, and then here within the function, I'm going
to
print out current underscore user. And so if I try to create a post now, and then open up my
console,
you can see that it printed out the user object, which not very helpful. But what we can do
real
quick is I'll just say current user dot email, we can see the email of that user, just to
verify
that we are actually getting the user. And we can see that it does print out the email. So
that's ultimately why this function exists. Because normally here, this is where you query
your
database to grab the user and then you return the user. And then whatever you return here is
ultimately what allows any of your other routes to get whatever you're returning. So
since we're returning a user, we're going to store it as a variable called current user. In
this video, we're going to take a look at some of the more advanced features of
postman.
And we're going to start off by taking a look at environments. So select the environments tab,
and you'll see that postman just gives you a quick description of what an environment
is,
it says an environment is a set of variables that allow you to switch the context of your
request. So we can set up some variables that change depending on what environment we work in.
And
you're probably thinking, well, what exactly is the point of that? And let me give you a great
example. If you take a look at all of our requests, you can see that we've hard coded them to be
127
dot zero dot zero dot one, port 8000. And this is our development environment. Now when we
ultimately
go to deploy our app, it's not going to be deployed on 127 dot zero, zero, one, two, we
deployed on some server, it's going to have a public IP somewhere on the internet.
And so when we want to test out our production server, or make some changes to our production
server, make some test requests to it, we would have to change the IP address, the port
number,
as well as we probably are going to be running HTTPS instead of HTTP. So all of our requests,
we'd have to change all of them, and then continuously flip back and forth every time
we move from dev to prod, and then from prod back to dev. And so instead of having to hard
code
these values, then change it constantly, we can actually create a variable that changes
depending on what environment we use within postman. So let's go to environment. And let's create
an
environment. And I'm going to call this environment, I will say this is dev. And then our
project name,
which is fast API. So this is our environment. And what we can do is we can define a
variable,
and I'm going to call this one URL. And then here, I'm going to actually just copy this
URL.
And then go to our environment again, and then I'm going to give it an initial value. All
right, and then our current value should update, and then we can just hit save.
Okay, and so we've set this variable to be this specific IP in our dev environment. And so if
we go back to collections, and then go to our get posts, first of all, now moving
forward,
you want to make sure that you're working in some kind of environment. So we'll select the
environment we just created. And then instead of hard coding this value here, I'm going to
change
this. And I'm going to just say, to actually use a variable, you do curly brace, curly
brace,
the name of the variable, and then curly brace, curly brace. Okay, and it should be orange
instead
of red, if you get a red, it's going to meet in that it wasn't able to resolve something, and
there's some issues. So just double check what you did. Orange is good. And then if
you
see this result, when you hover hover over it, that means that everything's working. And so
just to quickly test this, you can see that it says could not validate credentials, that's
perfectly
fine. I didn't set up the access token or anything. But this does confirm that we're able to
resolve that variable. And so that way, you know, if I go and then create a new
environment,
and I'll just add here, and then we'll say this is prod, past API, we can then give a
different URL
for the production environment. So this could be like, you know, HTTPS colon slash slash, I
don't
know, you know, some domain name that you buy, I'll call this Sanjeev dot x, y, z, slash. And
then I
can save this. And so that way, if you ever are testing your development environment, you
just
go to dev. And then when you want to quickly switch to your production, you just change that
and then the URL will automatically update. So you don't have to change anything
yourself.
And it's super handy to set this up. And we're just going to do this for all of our requests
moving forward. So I'm just going to copy this. I'm going to do this for all of them.
Okay, and if you go ahead and test this yourself, you should ultimately not run into any
issues.
I'm not going to bore you through, you know, individually testing these, you can do this
yourself. And if you run into errors, it's just you probably paste it in something
wrong.
Now that we've set up authentication on our API, testing our API has gotten a little bit
more
challenging. And that's because I can no longer just go to create posts, and then just hit
send, because I have to be authenticated first to be able to actually create a post. And so what I
have
to do in postman is I have to hit send, I have to log in, and then get the access token, I
have to
copy the access token, and then go to the specific endpoint I want to test, and then I want to
paste that in there, and then hit send. And then I can finally create post. And this is a little
cumbersome
and tiring. And it gets really hard if you have a short expiration time on your token. Imagine
if
you had an expiration time of five minutes, then every five minutes, you'd have to repeat this
process. But luckily, postman has a way to actually do this through an automated
fashion.
We're all developers, we all want to do things automated. So let's take a look at how we can
do this. And I had to make sure that you guys understood environments and variables
beforehand,
because this the method that we use actually makes use of environment variables. So let's go
to the
login user endpoint. And we're going to hit send and get a new access token. And so we've got
this
access token. And what I want to do is through code, I want postman to automatically set an
environment variable. So how do we do that? Well, let's go to tests. And here we can put in
whatever
code we want. And so we can read the documentation on how to do this. But the ultimate goal is
to set an environment variable through code. So how do we do that? Well, there's a little snippet
here
that kind of gives you examples. And what we want to do is we want to set an environment
variable.
And so to set an environment variable, you just do environment dot set, provide the key and
then the variable name. And so just like we did with environments, right, if you go into your
environment,
you've got the URL, which is the key and then the value. And so we're just going to do the
same thing, but just through code. So here, I'm going to set the key to be JWT. And I'm going to set
the
value to be this access token. So how do we actually retrieve the access token from the
response of the
request? Well, we can do pm dot response, that grabs the response object, then we'll convert
it to JSON. And then we need to get the token and the token sits on a property called access
underscore
token. So whatever this field is called, this is what you're going to pass in, we'll say dot
access
underscore token. Alright, and so now if I send this again, we'll get the token, the code
should
have run, and it should have set a variable called access token. And so now under create
post,
instead of hard coding, this, what I can do is I can actually just reference the JWT
variable.
And so I set minus capitals, there looks like there's one from a previous project, maybe. But
we want this one right here. And if you want to just double check, you can see that this
ends
in w y d three, oh, and if you want to go back into login user, you can see that this ends
in
w y d three, oh, so it looks like it did set that and updated accordingly. We'll set that in
there.
And so now if we send, you can see that we can now create posts. And so anytime you log in a
user,
it's going to update that token. And we can verify this, if you take it the last three
letters, q zero, a is the new token. And then I'm going to just remove that so we can see the value. And
we
can see that q zero, a is the last three letters as well. So this updates it dynamically. So
that
all you have to do is now just hit login user. And then you can start testing all of your
other
endpoints. And we're going to update all of the other endpoints as well. So forget posts, I'm
going to put that in there as well. And then for getting an individual post, deleting a
post,
updating a post, we don't need it for a create user. We might want to do a forget user if we
set
up authentication for get user, but we haven't done that yet. So we'll hold off on doing that
for now.
Now, currently, our application doesn't work like a traditional application
would.
And the reason I say that is, you know, if you take a look at how everything's been set up,
not just in our API, but in our database, we've got two tables, a user table and a post
table.
So we can create, modify, delete users, however we want, we can log in. And then we can
create,
modify and delete as many posts as we want. Right. And you're probably thinking, well, that
sounds perfectly fine. Yes. However, think about any other application, think about
any
social media type application, when someone creates a post, and you see that post on your
feed, what are you going to see next to the post, you're going to see the user that created
it,
right? So every post is ultimately associated with the user account that created that
post.
But we have no way to actually do that in our application at the moment, because there's
nothing that ties a post to the user that created it, right? Take a look at all of the columns in
our
database, we've got an ID, a title, content published, technically, there's a created that
column that I didn't draw in this diagram, but that's okay. So how do we know what post
was
created by what user. And this is where relational databases really start to shine, because
the main
idea behind a relational database is that you set up these relationships between tables. So we
need
to set up some kind of special relationship between the user's table and the post table
that
will allow us to associate a post with a specific user that created that post. So let's take a
look
at how we can do that within Postgres or any SQL based database. And the way we do that is
we
actually create an extra column in our post table. So we've got a column, you can call it
whatever you
want, but I'm going to call it user ID. And I'll explain why. And what we're going to do is
we're going to set up a special foreign key. And a foreign key is a how we tell SQL that this
column
here is connected to another table. And what we do is we specify two things. We specify the
table
that it should be connected to. So here I'm saying, hey, this should connect to the users
table. And then we specify what specific column it should use from that table. And here we're saying the
ID
column, because this connection is connected to the ID column of the user's table. And so
that's what we set the foreign key on. And all this does is this is really such a simple concept.
Whatever
user creates this post, we just embed the ID of that specific user. So for the post with the
idea
621, the user that created this has an idea of 212. So if we take a look at what user has an
idea
of 212, we go here and we can see that clay edge, email.com created this post. And so that's
all we
have to do to set up a relationship between these two tables. And so now moving forward, any
post we
create will easily be able to tell what user created it, because we're going to embed the idea
of that user into this column called user underscore ID. And if you take a look at the second one, as
an
example, we can see that this was created by a user with an idea of 378. So we go into the
user table, and we can see that this user is my get gmail.com. And it really is as simple as that.
And
this is what's referred to as a one to many relationship within SQL, or any
relational
database. And the reason why they call it that is because one user can create as many posts as
they
want. However, a post can only be associated with one user, two users can't create a post. And
so
that's why this is referred to as a one to many relationship, one user can create many posts.
And
that's all we have to do. It is actually that simple, we just create another column. And then
we have to specify this special foreign key constraint where we just tell SQL, hey, what table do
we
look for? And what column? And keep in mind, in this case, I'm using the ID of the users
table.
But when you get a little bit more advanced with SQL, and setting up relational
databases,
you'll find that it sometimes you don't have to always point to the ID of another table,
there's, there's going to be a lot of instances where you set up foreign keys to other columns that
aren't
necessarily the ID column, depending on how your relationships are set up and how your
application
should work. That is a perfectly acceptable thing. There's no specific restrictions saying
that it
has to point to the ID column of another table. And so I think that's enough theory behind
foreign
keys and relationships. So we're going to connect to our Postgres database in the next
section, and we'll start creating that extra column adding the foreign keys, and then not we'll start
learning
about how to actually work with these relationships. Okay, go ahead and open up PG admin. And
before
we get started, I'm going to do something a little unusual, I'm going to delete everything
from our post table. Now, when it comes to creating an extra column and creating a foreign key,
that's
not a requirement, you know, you don't need to do that in a production database, I'm just
going to delete everything just to keep just to make things a little bit more simple moving forward.
Because
if we don't delete any things, when you start to add columns that especially have a not null
constraint, we have to do a little magic to kind of get that to work. And I want to keep things
as
simple as possible. So we're going to do is we're just going to delete everything from the
post table just to make things as simple as possible. So we can just say delete from posts. And
that
should delete everything. And everything's gone. And we should be pretty much good to go to
actually
start setting up our foreign key. So right click on your post table, and then click on
properties. And the great part about these foreign keys is that, you know, we just have to do this on
the
post table, because it's a one to many relationship, there is nothing in the users table that
we have to do. So we're going to do is we're going to go to column. And then we're going to add a new
column.
And I'm going to call this user underscore ID. And you can once again, name this anything you
want, the actual name doesn't have to make sense. But, you know, I think naming it user underscore
ID
makes sense, because this is a column that's going to represent the idea of the user that
created our post, the data type. Now, this is important, because the data type of this column needs
to
match whatever the data type is of the ID column from the users table. So we always set the
idea
to be integer. So you want to match this. However, keep in mind that sometimes when you're
working with data spaces, it might be some other data type, right? If you use big int, you
definitely
want to make sure that this is also a big int. If you use a small int, make sure there's a
small int. If you use UUIDs, make sure that this is a UUID as well, you just want to match up
with
whatever that column is in that respective table. Now, we have the options to set this as not
null.
So right now, we can create a post with a null user, which means there's no user that
created
this post. And if and to figure out if you should set this to be not knowledge, it depends on
how
you want to set up your application. Should the database allow you to have a post without a
user,
it's up to you to decide I'm going to say no for now. I'm gonna say not no, because I don't
want to be able to create a post without a user. It doesn't make sense. And then after
that,
what we can do is we have to set up our foreign key constraint. So let's go to
constraints.
And then we want to go under foreign key. So this is where we set up that magical connection
between
the two tables, hit the plus sign, and then give this foreign key a name, the name of the
foreign key doesn't matter. Right? It doesn't impact the functionality or thing. This is more just for
you
as a user to be able to read it a little bit more clearly when you see it on the CLI. But
there's a
standard convention that we like to use when it comes to naming our foreign keys. What we like
to do is we like to take the table that we're working on. So it's going to be posts. And then we
do
underscore, then the table that we want to set a foreign key to. So it's going to be users.
And
then we just do underscore f key. Once again, this is just nothing more than a name to
describe the
foreign key. It's not actually doing anything at the moment. To actually set up the logic of
the foreign key, we have to open this up. And we have to go under columns. And so this is how we
actually
set up the relationship. So here we're saying, what is the name of the local column? The
column
in our posts data table, what we created our brand new user underscore ID, then it's going
to
reference what table? Well, we know it's going to reference the user's table. So we select the
user's table. And then what column from the user's table is it going to reference?
Well,
it's going to reference the ID column of the user table. And at that point, you just hit
plus,
it gets added up here. And before we move on, there's one last thing I want to
cover.
Because we're almost done, we have to go under actions. And because we're setting this
relationship
up between two different tables, we have to figure out, you know, what happens when a user
gets
deleted, right? Because right now, a post is going to have the idea of the user that created
it. What
do we delete that user? What do we do? So if you go to the on delete section, we have a couple
of different options. One of the more common ones is cascade. And so what cascade does is, if I
let's
say I if I have a couple posts that were created by a user with an ID of seven, if I delete
that
user, then Postgres will automatically go into my post table and delete any posts that were
created
by that user. And so that's one option. That's what I'm going to use. However, we have
other
options as well. We can set it to the default value of a column. So if we give the column
a
default value, maybe we create like a like a random ID of zero or something, then it's
going
to assign all of those posts that were created by the deleted user to zero, we can set it to
null.
So if we did set null, then it's just going to set the user ID column to null. If that user
gets deleted, keep in mind, if you want to be able to use set null, then you have to go back to
columns
and make sure this is not set to allow you to set it to null. So we'd have to set it to no, or
then it would throw an error when you try to delete someone.
And then there's a few other options. But we're going to stick to cascade. And then keep in
mind, you can do the same thing with on update. So what actions do you want to perform when you
update
a specific user in that case, but we'll leave it as no action. For now, I just mainly care
about the on delete. At this point, that's all you have to do for foreign keys, you just specify the
column
of the other table, it should point to and you're good to go. We'll hit Save. And then we're
going
to go under posts. Well, actually, before we do that, we're going to go under users, we're
going to view all rows. And let me clear out some of these, there's too many windows open, right?
And
these columns got a little squished. But right now I have three users, it's got an ID of
171819. So
go ahead and remember that you obviously your database is going to be different. So just
remember the IDs of a couple users. And what we're going to do is we're going to go under our
posts.
And we're going to view edit. And so right now I have no posts, because I deleted
everything.
So I'm going to create a brand new post. And I'll just call this my first post. And the
content is just gonna be some random content. I'll leave published blank, because
we've
got a default value. And then right now, if you take a look, we've got this new user ID
column. So if I try to save a post right now, take a look at what happens, it gives me an
error,
it says null value and column user ID vol violates not null constraint. So because we said
that user
ID cannot be null, we have to provide a user ID. So let's give it an ID. And if we go back to
users,
I can see that there's going to be I can use 17, 18 or 19. So whatever user created this, I'll
just say john created this, we'll grab an ID of 17. And I'll say that this is going to
be
an ID of 17. And then if we save this, look at that, we now have a relationship
between
the users table and the post table. Let's create another entry. We'll call this second
post.
And it's going to have some random content. And I'll say that this was created by the user
with an ID of 18. And that should work just fine. Now what I'm going to do now is what
happens
if I create a post and I give it an ID of a user that doesn't exist. So I have IDs of 17,
18,
19. What happens if I set the user ID column to be a value of 20? Well, let's let's try that
out.
So I'll call this third post. And we'll set the content to be something
random.
And if I set this to 20, let's see what happens. I hit save. Look at this insert or update on
table
posts violates foreign key constraint, there's no user with an ID of 20. Right? So that
doesn't
exist in that table. So it's going to throw an error. So that's the magic behind foreign keys
that it's going to check to make sure that that user actually exists. And so this isn't going
to
work. So we'll just set this to 17 as well. Now save that. Right. And that's it, guys. And
when
it comes to, you know, kind of querying these users, right, let's go back to our
database.
And let's just set up a query. Now let's say I want to get all of the posts created by user
17. Well, we can do select star from posts, where, and now instead of, you know,
searching
for based off ID, we grab the user ID column. And we set that equal to 17. So this is going to
get
me all of the posts that were created by user 17. I run that you can see that this got the two
posts
from user 17. And then if I change this to 18, we're going to get the one post created by user
18. So from the SQL perspective, right, nothing's really changed, you just specify what column
you
want to match on and then provide a condition. Now, what I'm going to do now is I'm actually
going to
delete this user. So I'm going to go and just say, delete from users, where ID equals 70. So
we're
going to delete john edge email. And we're going to actually do that here. So I'll say, first
of
all, select star from posts. All right, so we've got all three posts. And I'm going to run
another
query, except I'm gonna say delete from users, where ID equals 17. So what is going to
happen
when we delete those users, because we have posts that have relationships to them? Well, since
we set the on delete to cascade, it should delete these posts automatically for us. So I'm
going
to leave this down here. So it's going to run another query so we can see exactly what our
table looks like after we delete this user. And so if I run this, you can see that when I run
a
select star from posts, there's only one entry left. And that's the one with the user ID of
18, because it automatically deleted all of the posts created with an ID of 17. All right, and so
from
a database perspective, I think this is all we really need to cover for now. Eventually,
we'll
start taking a look at joins, which is how to run these complex SQL commands that allow you to
jam
the columns of multiple tables into one result to make it a little bit easier to retrieve
information.
Because right now, if I, if I try to get all of the posts, you know, like send me every
single
post on my database, right, it's just going to give me the user ID. But if I need the
information about the user, like what's the name of the user, what's the username or the email, then I
would
have to individually query the the IDs of these users. So I'd have to go then I'd have to
do,
you know, a select, whoops, make sure I don't delete anything. I'd have to do a select
star
from users, where ID equals 18. And I have to do this one by one for every single
post.
So that I can get this specific user. And so that's when we start to make use of things like
joins. But that's a little ahead of us at the moment. And that takes a little while to
explain.
So we're going to come back to that. But before we actually kind of move on from this database
side
of things, because we pretty much covered everything, we're going to delete that column for
now, because we're going to make sure that SQL alchemy actually sets up all of these
constraints
for us automatically, so that we don't manually have to do it. So let's go to our properties,
go to columns, we can just delete this column. And then I think this should automatically
delete
the foreign key, it does not delete the foreign key. So make sure you delete that as well. And
then we can save that. And it looks like there's thrown an error.
So let me cancel out of that. And I think we're gonna have to do this in a two step process.
So we'll say constraints will delete the foreign key. First, we'll save that and then
properties will delete the user ID. There we go. And so now if I do a select star from
posts,
right, there should no longer be a user ID column. So this put our database back into the
state it was in before we started playing around with it. So now that we know how to create
and
set up a foreign key within Postgres and PG admin, we're going to see how we can do
this
through code using SQL alchemy. Because ideally, since we're already using SQL alchemy to
generate
all the tables, generate all the columns for each table and all the different properties, we
should also set it up to the generate the foreign key for us as well. So we're going
to
go to the models at p y file, this is where we define what the tables are going to look like
with the different classes. And we're going to add a new user ID or owner ID column to the
post
class. And that's going to create a specific column within the posts table. And so we
can
just go here and you know, call it owner ID, user ID, whatever you want, I'm just going to
call this owner ID. And this is going to be column. And then here we have to specify the data type of a
column.
And as I mentioned, the data type should match up with whatever the data type of the foreign
key
is. So since this column is going to point to the ID column of the user table, it should match
up
with whatever is here. So since this is integer, this has to be integer as well. Then to set
up
a foreign key constraint, you just type in foreign key, and then go ahead and auto import that
if you
don't know where it's getting imported from, it's going to get imported from SQL alchemy. So
if you didn't automatically do it, you can go ahead and just manually type that out. And then there's
going
to be two fields that we're going to pass in. So the first of all, we have to pass in the
exact column and table table and column that we want to reference. And so we want the users table and
then
grabbing the ID column. And so your instinct would be to use the class name, but we don't
actually reference the class name instead, we want to reference the table name. So it's going to
be
lowercase in this case, so you say users dot and then what's the column name, it's called ID.
So we
say dot ID. And then the second thing we need to do is if you recall, we have to set up what
the
policy is for when we delete the foreign key or the parent table. And so we always used
cascade,
so that if the parent object gets deleted, all of the child object objects get deleted as
well. So we can do that through SQL alchemy as well. And we'll say on delete equals and then we just
say
cascade. And then finally, we want this field to be nullable equals false. So it has to be
filled in.
All right. And now if we save this, restart our application, we'll take a look at
Postgres.
And if I actually refresh this, and this is actually an old,
an old panel, so I'm going to remove that. And then if I just do, we'll go to our post
table,
go to query tool, I'll say select star from posts. Run that, you can see that there is no
actual owner ID column. And so the reason for this
is that SQL alchemy, when it when we start our application, SQL alchemy, we'll check to see
if
there's a table called posts. And if there is, oh, sorry, if there isn't, it's going to then
create a
table based off of these rules. However, if there's already a table named posts, it's not
going to do
anything. So if we update any properties for a pre existing table, it's not going to change
that. That's not exactly what SQL alchemy is meant for. Instead, we would have to use a database
migration
tool, kind of like alembic, which we haven't covered yet. So instead, for now, what we're
going
to do is we have a couple of different options, we can manually just do it, go into PGA admin,
add the things yourselves. Or we can just take the easy way out. Since this is a development
environment,
we can just drop our post table. And that's one of the luxuries of working in development.
So
you can then hit Save again. And that's going to cause it to restart the application, the
application.
And then if we go to Postgres, and we just hit refresh, it's going to create a new post
table.
And then we can go into properties. And we'll go into columns. And we now see that we have an
owner ID column, which is set to not null. That's good. And then if we go to constraints, foreign
key,
you'll see that we've got our foreign key. And you can take a look at the details down here.
And the main thing I want to check is for actions, we do have on delete cascade. All right, and
so
now that we have everything set up, if I just run this query again, we should see the owner ID
show
up. So let's create a few entries. And before I do that, I need to make sure that I actually
have
some users, because I kind of went in and deleted a few things. So I've got two users with an
ID of
20 and 21. And so what we're going to do is we're going to make sure that the foreign key
points to
one of those IDs. So we'll grab a post, I'm going to just set this to be post one as the
title. And it's going to be some gibberish. And then we can set the post, the owner ID to be the ID of
a
user that we already have in our database. We'll save this. And we can see that it's
successfully
created. Now if I create a new post, and this time leaving the owner ID blank, if I hit
save,
you can see that it throws an error because this can't be set to null. And if I try a ID of a
user
that doesn't exist, like 57, it should also throw an error. And so it's saying that hey, the
owner ID of 57 does not exist in the user's table. And I'll just change this to 21. And then it
should
work. Great. Now let's quickly check out the on delete functionality just to make sure that
it
works. And what we're going to do is let's go to well, I'll just run the query right here.
We'll
do delete from and we'll say users where ID equals and we're going to delete the user with an
ID of
20. So if everything works, we should see just this entry also get deleted in the post
table
because the owner is has an ID of 20. And since that owner is going to be deleted, then we
should
only have this one left. So if I run this, yep, we can see that the one with the owner ID of
20 is now deleted. All right, and so that kind of handles all of the database side of things. At this
point,
if you try to test the rest of your application, you're going to probably run into a couple of
issues. But that's just because you know, we've hard coded our schema to match a certain,
you
know, number of properties. And so since we've added this new property to our posts, we're
going to throw a few errors. So we're going to have to update a few things in the next video. But
you'll
see that updating your schema is very simple. With the changes we made in the last
lecture,
it's inevitably going to cause some issues with our application. So there's going to be a few
things that we have to update within our app. And so let's just quickly run through a couple of
our
requests just to see what they look like, see what changes we need to make. And so if you
haven't already done so, log in a user. And so once we're logged in, we should update the variable. And
now
we can retrieve our posts. So if I hit send here, you could say that we retrieve all of the
posts in our application, I just have one in this case. But the first issue that I see is that we're
not
returning the owner ID, right, this is a brand new column. And I would expect this information
to get
sent out to the user. And the reason why it's not included in here is that we have to actually
update our schema, because our schema probably doesn't have that own right, because we never
added
that in. And you might be thinking, well, do we actually need to send it? Yeah, I think it
makes sense for us to send it because any application the user should know who creates a post.
So
ultimately, they're going to have to get that information. And so we should provide the owner
ID. So let's go to our code. And if you actually go to the posts, router, and take a look at
the
get all posts, you'll see that the response model is going to reference schemas.post. So let's
go
to schemas.post. And you can see that this is what we're using to return. This is the schema
that
we're going to use for returning post to a user. And so this actually inherits from post base.
So
post base has title content published. And then we extend that and we're going to add the
ID,
which gets created at the database level, as well as the date and time or the created that
field, which gets added by the database. So we can add it here. However, I'm sure you'd
think
well, could we add it at post base? Should we add it post create? Well, let's think about
this. The first thing that we have to think about is, when we create a post, should we be passing
in
a owner ID? Well, that we 100% absolutely could do that. And that actually does kind of make
sense.
So in that case, if we did want it to be available, or require it for creating a post, then we
would
add it under post create here as well. Or we could just add it under post base so that both of
them
inherited. However, what we're going to do is we're actually not going to require the user to
provide the owner ID. Instead, what we're actually going to do is we're going to let the the
logic
of our route to actually just grab the ID from the token, and then use that as the field. So
we
don't actually need the user to pass that into the body. So we're not going to, we're not
going to use that field or apply that field to either one of these classes, we're just going to do it
for
the post class, which is the one that's responsible for sending the post out. So I'll add a
column
here. And we'll call it owner ID. And I'm going to say that this is going to be a type of
int.
Alright, let's save that. And let's see if that makes a difference. So let's hit send. And
so
now we can see that we get the owner ID. And that's really all the changes we have to make so
if we actually go to get one post, and we look at our code here, and if you look at the
post.py,
for getting an individual post, you can see that we're returning the same exact model. And if
we
take a look at creating a post, we return the same model. So all of those will be updated
accordingly. And same thing with the update post, the update post also uses schemas.post. So
since
we're using the same schema for all of them, we don't have to do anything else. And we could
just quickly test that out. So I'll do get one post. And I realized we have to actually get what
was
the ID ID is for so we're going to update this to be an idea for you can see that we now have
the
owner ID there, we update it. Right, we can once again get the owner ID. And at that point, it
looks
like everything else is good. However, if we go to create posts real quick, and we create a
post,
right, we get a crash, actually. So there is an issue with creating a post. And I'm sure you
guys
can guess exactly what it is. Take a look at the error when we try to create a post. Right,
it's
saying there's some kind of SQL error. And it says there's a null value in column owner ID.
And that
makes sense. And that makes sense. So we'll actually tackle this in the next video. And you'll
see that it's going to be pretty straightforward to actually get that resolved. So in the last video, we
learned
that the create post functionality is broken now. Because if I try to create a post, you can
see
that it sends back in a 500 status code. And if we take a look at the logs, we can see that it
looks
like there's some issue with the SQL. And it's saying that the null value in column owner
ID
violates the not null constraint. And so looking at our model, we obviously set the owner ID
right
here. And this is set to be nullable false. So we actually have to provide who the owner ID
is
for this new post. So how do we do that? Well, let's go to our post path operation. And
let's
go to the create post path operation. And so it's right here. And so nowhere in this code are
we
actually providing a user ID or owner ID into this new new post that we're creating, we're
just
grabbing the post, which comes from post create the schema. And if we look at that schema, you
can
see that we do not provide a owner ID. And like I said, we're not going to actually pass the
owner
ID into the in the body, what we're going to do is whoever is logged in and creating the
post
should automatically be the owner, right? If you're logged into Twitter, and you post
something, Twitter knows that you're the one creating the post. So whatever ID is associated with
your
account, it's going to set that automatically. So we shouldn't have to pass that in the body,
we should automatically retrieve it from your authentication status.
So within post, right, you can see that we have the current user. And so we should be able to
get
the current user or the user's ID. So if I do current underscore user dot ID, and we can
remove
this kind of need the email. And if I tried to create a post, it's still going to error out,
that's okay, I just want to see what it prints out though before the error. And you can see
we
got a lot of errors, that's okay. And you can see that it printed out the ID of my user, which
is
23. And just to double check that that's actually my user, I'm going to select star from
users.
And you can see that 23 is Sanjeev at Gmail. So we could just take that and just add that
into
the new post object. And so we're creating the new post right here where we reference models
that
post. And what we're doing is we're just spreading whoops, we're just spreading out the schema
that
we got from the body. And so to actually add the ID property, it's very easy. All we have to
do is
just say, owner underscore ID is going to be set to current underscore user dot ID. So we're
just
grabbing that from the get current user function, just like we did right here. And I think
we
probably need a comma there. And that should be all that we have to do. So let's, I'm going
to
remove this code. And let's test this out now. So if I do create post, look at that, no
errors,
you can see that the owner ID was automatically set to the ID of my user, which is 23.
Currently
in our application, we do have authentication setup for delete post and update post.
However,
there's no check to make sure that a user is only deleting his own posts. Right now, if
you're
logged in, you can delete anyone's posts. And no application works like that, you should only
be able to delete your own post, no user should be able to come in and delete one of my
posts,
that doesn't make sense. So let's implement the logic for setting up a quick little just if
statement just to check, hey, is the person that's logged in trying to delete a post
that
he owns? If he doesn't, then we're going to return an error. And so right now, if you take a
look at our delete post path operation function, we query the post that he's trying to delete. And we
check
to see if there's no post, if there's no post, then we send a 404 that's expected. But if we
did find a post, the next check is just going to be another simple if statement. And we're going
to
say if post dot owner underscore ID does not equal get current user dot ID. So these two
things
have to match for the user to be able to delete it, if they don't match, then we're going to
send another HTTP exception. So we'll say raise HTTP exception. Now the exception will set the
status
code to be a new one. So this one's going to be a 403. So this means forbidden. That's the
means
it's a resource that they specifically aren't able to access because we could send a 401. But
they
are, I guess a 401 could also work. But I'm going to do 403. I think that makes more sense.
And then we can set the detail. And I'm just going to say not authorized to perform requested
action.
And this exact same check can be used for updating posts as well, because this is the same
exact logic. And I'll just do that right here, right under the same check. So same logic, right?
First
thing we're gonna do is we're going to check to see if that post exists. And then the next
thing we're gonna do is we're going to make sure that the owner of the post is whoever the user
is
logged in as. Now let's test this out. So right now, if I do a get posts, you can see that
the
owner, okay, so we've got a post created by both owners. And I'm currently logged in as I
actually
can't remember who I'm logged in as. But we can see that I used Sanjeev at Gmail, and that's
going
to be user 23. And what I'm going to do is go back to get posts. And I'm going to try to
delete the
post with an idea for because that's the owner ID of 21. And keep in mind right now, I'm
logged in
as user 23. So let's try this, we're going to delete post with an idea for Okay, it's
already
set there. So we should get an error in this case. And it looks like I got a server error,
that's a
problem. And I already see the issue. And right now, post is actually not the post itself,
it's
the query. And to actually get a post, I have to do post dot first one. So what I'm going to
do
actually is I could change this to post dot first, and then grab the owner ID. But I'm going
to
actually perform the query right here, I'll say post. Actually, I'll change this to post
underscore
query, we're gonna make a few changes, actually. And I'm going to set this to be post equals
post
underscore query dot first. And then I'm going to set this to be post. And this is going to be
set
to query. And just to kind of quickly recap, we define the query here, we'll then find this
post,
we'll check to see if it's not there. And then we'll check to see if the owner is if the
user
who's logged in actually owns this post. And then we're going to grab the original query. And
then we're just going to append a delete that we delete it. That's all. And let's just make sure the
update
is set up the same way. And it looks like the update one was already working that way. So it
should be good to go now. Hopefully, there's no errors. And let's try this. And once
again,
we got an error. And it looks like function object has no attribute ID. And I realized my
mistake,
again, first of all, this is getting stored as current user not get current user. So we need
to
change this to be current user, I'm not sure why I did it like that seems pretty goofy. And
then
this also should be current user. Right, because we're calling that function and we're storing
the
result in current user. So we want to reference the variable. I'm not sure if that was like an
autocomplete that did that or I just had a brain fart either way. Hopefully, it's the last of
our
issues. So let's try this. All right, perfect. Look at that not authorized to perform
that
requested action that's to be expected because we don't own that post. However, if I do a
quick search again, I do own the post with an ID of eight because we can see the owner ID is 23.
And
that's who I'm logged in as. So let's try to delete post with a at the value of eight. Send,
and we get a 204. So that means it's successfully deleted, we'll do get posts just
to verify. And we can see that we get those are just the one post now. So that seems to be
working.
I'm going to go back in, actually, let me create a post real quick, because I no longer have a
post.
Alright, so we have we created a new post. And so you can see that this is my post right here.
And I'm going to just update it. So let's go to the update one. First of all, I'm going to try
to
update a post that I don't own. So we're going to try to update the post with an ID of four,
we should get a not authorized. And then let's grab the ID of the post that we do want to
change,
which is nine. Let's hit send. And it looks like it updated a little bit. Let's check our
database
always. So we'll say select star from posts. And the post with the nine got the updated
entry.
Oh, wait, wait a minute, it looks like it may have updated both of them. Only one way to
find
out if that's actually what's happening. I'm just going to create a quick new
post.
I'm just going to set the ID to be 23. Alright, and I'm going to update post with nine with an
ID of nine. Okay, and then let's refresh
this. Okay, so post with an ID of 10 is still the same. So okay, it wasn't a bug. It looks
like maybe
I had changed this earlier. And I'm not exactly sure why I had when I updated this post, but
that's
okay. And so I think that's going to wrap up this video. We've handled the update and delete
so that
we can only update and delete our own posts. Alright, before we proceed any further, I do
want
to talk about one last thing. And that is that when we do get posts, which is an authentic
post,
an authenticated route, so it requires you to be logged in to retrieve the posts. If I try to
retrieve the post, you'll see I get posts from every user. Now, this may or may not be
the
result that you want, right, it's going to depend on what your application is going to work.
Look like, you know, if this is like a, you know, like a note taking app or something where all
of
your posts and things like that are all private, then you wouldn't want to return everyone
else's posts, you'd only want to return the user, the user specific post. However, if this is some
kind
of social media app, right, then obviously, your posts are public. So when you do get post,
you'd want to return everyone's posts. And so it really just comes down to how you want your app
to
to function, I'm going to show you guys what you would need to change if you wanted to make it
so that you returned your only your own posts. But after afterwards, we're going to actually
delete
those changes, because I want to leave my app like this, cut, this is kind of how I wanted it
to work more like a social media app. But I do want to make sure that you guys understood how to make
this
change. And the same thing would go for get one post, we want to make sure that only the user
that created a post can retrieve that specific post. And so if we go back to our path operation
for
getting all posts, you can see that we just do a db.query.all. And so if you only want to
return
the posts for a specific user that's logged in, it's very simple, all we have to do is do a
filter. And here, we'll say models dot post dot ID equals equals. And then we just get the current users
ID.
And it looks like I still left this as user ID. So this should actually be updated to be
current
underscore user. And if you notice the you can see that the type is set to int, even though
it's
actually returning an entire user object, I found that this whatever type you put here doesn't
actually impact the code. So put whatever you want, you can put in a you know, some sort
of
dictionary that actually correctly matches it. But you could just leave it as well. So I'm
just
gonna leave it like that. It doesn't break any logic. And then at this point, I believe I'm
still
logged in as user 23. If I try to retrieve all posts, looks like I got nothing. And that
could
be a little bit of a problem. So let's see what we broke. And the easiest way to kind of
troubleshoot this is what I'm going to do is I'm actually going to remove the dot all. So that's going to
return
the query, it just won't actually run the query. And I'm just going to print the query. Let's
see what that looks like. Sometimes it helps to see the raw SQL. Let's hit send. Alright, we
should
get an internal server error that's to be expected. But I just want to see my print
statement.
Alright, so let's see our print statement. So select, we're getting all of the columns that's
expected from posts where posts ID equals this value. So that looks like it's perfect.
Well,
let's just quickly see before we move any further, what exactly is my user ID. And we'll just
check
our database. Okay, and we'll run this again. And then we're going to check. Alright, so 23.
And
then if we go to our database, and I realized the exact mistake that I made, and this is a
stupid
mistake, right, this is this is looking for a post with an ID with this specific ID, we
don't
want the the ID of the post, we want the owner ID. So we'll do owner underscore ID equals
current user ID. All right, let's test this out now. Once again, an error, but I just realized I forgot
to
update this to be dot all. And then we'll try this again. And now we get all the posts with an
ID
of 23. If I log in as a different user, let's say this is Sanjeev one login that should update
the
token. So now if I do a get all posts, it should return all posts with an ID of 21. And so
that's,
that's how that works. And then if you want to see how that looks like for retrieving an
individual post, we'll go to get underscore post. And to actually handle this logic, it's gonna be
the
same thing as a delete. So you can just copy what we did for the delete one. And here I can
just say
post dot owner. And this should do the same exact thing. So now if I do get one post, well,
first of
all, let's take a look at all of our posts. I'm going to do it in our database. So I'm logged
in
as user 21. So I should be able to access a post with four, but I can't access a post with an
ID
of 10. So let's try 10. Am I able to access him? Not authorized. Let's try idea four. And I
can get
that one. So that's how that works. But like I said, I don't want my application to work like
that. So I'm going to remove that functionality, I'm going to make all posts essentially
public.
And then we're going to remove this filter. And I'll remove this print statement and this
print statement just to clean things up a bit.
And let's just double check that we didn't break anything once again. So let's get all posts,
we get all posts. And let's get an individual post, regardless of who owns it. And that's
all
fine and dandy now. Now, just kind of thinking ahead of what our, you know, front end would
look
like even though we're not going to build it, taking a look at any social media type
application, you know, when we retrieve the posts, we usually want to embed the user's ID. So we want to
know
who actually created the specific post, we wouldn't just put the owner ID because no, no user,
none of
your users understand that ID, they want to see what is your, you know, Twitter handle, what
is your email, whoever created that, we want to see their user ID. So it looks like for all of
the
posts that we retrieve, we would have to then send a second query to retrieve, you know,
hey,
what is the information for user with an ID of 23. And then once we get that, we would then
have to
kind of combine all that data so that we can figure out what post it belongs to what user and
what is their specific username. Now with SQL alchemy, we can kind of set up,
set it up so that it automatically does this for us. And so if we actually go to our
models,
what I'm going to do is I'm going to set up a relationship. And so this relationship
isn't
a foreign key, it does nothing in the database whatsoever. But what it does is it'll tell SQL
alchemy to automatically fetch some piece of information based off of the
relationship.
And so here, I could say, we'll create a owner. And I'm going to say owner equals
relationship.
And we probably have to import this. And so it did. So imported relationship from SQL
alchemy.ORM.
And so if you haven't done that, go ahead and do that. And what we'll say is, this is going
to
return the class of another model. So here, I want to return the user. And this is going to be
a
capital U, because I'm not referencing the table, I'm referencing the actual SQL alchemy
class. So what this is going to do for us automatically is that it's going to create another property
for
our post, so that when we retrieve a post, it's going to return a owner property. And what
it's
going to do is it's going to figure out the relationship to user. So it's going to
actually
fetch the user based off of the owner ID and return that for us. And there's nothing else
we
have to do it actually is that simple. This is one of the great parts about SQL alchemy is
that these relationships will automatically make it so that it fetches that data for us so that
we
don't have to manually do it ourselves. So let's test this out. Let's see if this actually
works.
I'm going to go back, and I'm going to retrieve all posts. And it looks like nothing's
changed.
We don't get any user information we do once again, just get the owner ID. So what happened?
Well, you probably can guess we need to update our schema. And so if we go to our
post,
we're now going to return a user. And so here, what we would do is we would just add
another
property called owner. And then here, it would instead of returning an int or a string
or
anything like that, we can actually return a pedantic model. So I can say I want you to
return
a user, whoops, a user, but it looks like we don't have a user class, we have user out. And
then we
have user create. So which one do we actually want to return? Probably user out because
that's
why we're returning with the user create is for creating a user. And you notice that we get
an
error actually here. And that's because this user out hasn't been defined at this point in the
code.
So you have to read Python top down. And so user out is actually defined all the way down
here.
So if you wanted this to work, we would have to move all of the user stuff up a level. So
I'm
going to cut this, and I'm gonna just put it right above post. And so this should remove the
error.
And so now it looks like it's good. And so once again, all we did was we added a new property
called owner. And this is going to return a pedantic model type called user out. And so
that's
all this is returning now. So let's see if this fixes our issue. And now I'm going to
retrieve
all posts. And so now take a look at this, it automatically fetch the owner, it got the
ID,
it got the email, it got when their account was created at, we may not want that. But for
now,
I think this makes sense, we could create another class to kind of narrow down the exact
fields that
we specifically wanted for this situation. But I think this is good enough. These are all the
information that I want actually returned. So this is perfect. It's going to do this for
every
post. And if I try to get one post, it's also going to return that specific owner. And so
in
reality, to get this functionality, right, once again, all we had to do was just create this
relationship. And so once SQL alchemy understands the relationship, it's going to fetch that
piece
of information for you from the user model. In this lesson, we're going to take a look
at
query parameters. And if you've never worked with an API, you may not know what that is. But
I
guarantee you, you have worked with them before. And you have seen them, you just had no idea
what they were. And so I'm on Yelp.com. And I'm just doing this for demonstration purposes. But
I'm
just going to do a quick search here. I've just picked a random city. In this case, it's
Miami, we're going to search for that. And I want you to take a look at the URL. And so our URL is
first
of all, we have the domain name, which is kind of like the IP address. And then we have the
specific endpoint that we want to reach. So this is the slash search endpoint. And so in their API,
they've
set up, you know, some sort of endpoint that probably is allows you to search for
restaurants.
And then we have a question mark. And I know you guys have seen this before, because pretty
much any website you've ever, you know, used, you'll see that question mark whenever you're, you
know,
searching for things. And any results you get, you'll see that in the URL. And so everything
to
the right of that is what's referred to as query parameters. So all of these are, you know,
query parameters. So all of this is query parameters. And a query parameter is a
optional
key value pair that appears to the right of the question mark. And these query parameters
allow
us to kind of filter the results of a request. So you know, if we're trying to retrieve
posts,
maybe we don't want all posts, maybe we want to get posts that were created in the last two
hours, maybe we want to get posts that, you know, if it's a social media type app, maybe we want to
get
posts that have received over 100 likes, right, these are all things that you would do using
query
parameters, where you can say, hey, you could just pass in a key and a pair, and you can say,
I want to find posts, you know, that are less than two hours old. And so a lot of other operations
that
are necessary in an API, things like pagination, those are all going to be done with query
parameters. And if you take a look at this one, since we searched for Miami, right, it looks
like
it passed a query parameter called find underscore loc, which probably means location. And
then it says Miami, Florida. And so it's going to basically talk to the API and say, Hey, listen, I need
you
to get me all the restaurants. And I want you to filter down based off of restaurants in
Miami. And so that's kind of how query parameters work. And so for us, it's, it's up to us to define
what
query parameters we want to allow and what we want them to do. And it's going to vary from app
to app. But you'll see that most API's have a couple of query parameters that they all use. So let's
go
to our app real quick. And what we're going to do is we're going to go to our post router. And
in
this case, we've got our path operation of retrieving all posts. And what I want to do is I
want to let the user be able to kind of filter down on the post that they want to see. And
the
first thing that I'm going to do is I'm actually going to allow them to specify how many posts
they want to retrieve altogether. So I want to give them the option to say, Hey, I want 10
posts,
or maybe I want 100 posts, or maybe I want 50 posts, we should allow the user to define
that.
And so to to allow a query parameter, we could just go into our path operation function
and
just pass in another argument. So I'm going to do question mark, then we give it the name of
the query parameter. So this is the key essentially. And I'm going to call this limit.
So this is going to limit the number of posts they get. And this is going to be of type int.
And we're going to give it a default value. So we'll say that, you know, by default,
if they don't provide a limit, we're going to say the limit by default is 10. And now
I'm
all I'm going to do is I'm going to print out limit. All right, and I'll show you guys how to
actually send that query parameter. So let's go to our get all posts. And so to send a
query
parameter, it's very easy, you just type in a question mark, then you grab the name of
the
query parameter, which once again is limit. So we'll grab limit. And then you say it's you
set
it equal to whatever value you want to be. So if I want to get a limit of three, if I hit
send,
right, nothing should have changed in our code. But you can see that we were able to print out
the limit. So that's how we access query parameters in fast API. It's pretty simple, you just pass
it
in as another argument into your path operation function. But let's actually set up our query
so that it now takes into account the limit. And with SQL alchemy, anytime you want to, you
know,
perform another operation, you usually have a built in method. So I'm going to remove this dot
all for now. And so if I want to limit the number of results, I just do dot and then let's see
what
methods we have at our disposal. Since we're looking for something that limits something,
let's maybe check to see if there is a limit method and looks like I don't see one
here,
but there actually is. We can do limit. And then here, I'm just going to pass in the limit
variable.
And then after that, we can just do the dot all as we usually do.
All right. And so now, well, first of all, in our database, we don't actually have that many
posts.
So I'm just going to create a couple. Well, actually, I could just do this through our API lab
quicker. So I'm just going to create a whole bunch of posts. And so that should give us
five
posts. Now let's create a few more. All right, so we've got 11 posts now. And then we'll go
back to
our API. And we'll say I want a limit of three. So let's send that. And let's see if we only
get
three posts, we get 123 perfect. And if I don't provide a limit, well, actually, let's try
a
different number. Let's try five now. Alright, we get 12345 perfect. And if we don't provide a
limit
all together, it should return 10 because our default was set to 10 posts, the limit of
10.
And so we get 12345678910. And there are 11 results. So there's one result that it didn't
provide. So
we've got the limit functionality down, what I want to do now is allow the user to skip
results. So
it grabbed, you know, depending on however, Postgres has decided to return, you know, 10
posts, it grabbed the first 10 based off of some criteria. And if we actually take a look
at
how it looks like we got ID of 10, ID of four, so it's kind of just all over the place. So
it's
probably not sorting based off of any specific field, we may want to specify that. But what
we're
going to do is I'm going to actually set the limit equal to two. And so we'll get the first
two,
however, Postgres determines what are the first two which should send, we get to, but let's
say we want to skip over a couple of them, maybe we want to skip the first, the first two, maybe
we
want to skip these two. Well, I want to be able to send another query parameter called skip,
so that
we can specify how many we want to skip. And if you want to send more than one query
parameter, we just do the n keyword, and then we provide the next key value pair. So it would be like
skip
equals two. And so just taking a look at the IDs here, we've got one with an ID of 10 and one
with
an ID of four. And we want to skip both of those. So going back to our code, we're going to
provide another path operation function, sorry, another argument into the function. And we'll call
this
skip is going to be of integer. And the default is going to be we'll say zero, we don't want
to skip
anything by default. And to add this to our query, we use the keyword offset, we do offset.
And then
here I'm going to say offset equals skip. Alright, and so we should be able to skip over both
of
these. And so if I provide a skip of two, you can see it first starts out at nine, and then
11.
If we do skip zero, it shouldn't skip any right 10. And then an ID of four, if we skip one,
it
should just skip the 10 and then start off on four. And we can see that the first one is four.
So it allows us to skip over post. And so that's ultimately how we're going to implement
pagination
on the front end is because the front end should be able to skip based off of what page
they're on. So if each page returns 20 results, and you want to go to page two, then you want to skip
20
results. If you want to go to page three, you would skip 40 results. Now, the last query
parameter I want to set up is a search functionality, I want the user to be able to search based off
of,
you know, some keywords in the title, or maybe even the content, depending on how we want to
implement searching for, for now, we're just going to say we'll be able to search based off of
the
title. And the way that the search will work is we'll have another and, and we'll say
search
equals and then you know, some random text. And so once again, we're going to provide another
argument called search. And this is going to be of type string. However, this is, you
know,
we can't exactly give a default search. So I'm going to say this is
optional.
It's gonna be an optional string, but we don't have to provide this.
And the default is just an empty quotes. And it looks like I have to import
optional
from fast API or from typing. And so here, I'll just say, import
optional.
And this is going to give us an optional query parameter. And so what we're going to do here
is
I'll say filter. And to filter this, and just kind of like how we filtered based off of a
specific
user ID or a specific post ID, I can say models dot post dot title. And then we can use a
method
called contains. So we can say contains, and then the search keyword. So this will allow us
to
provide some kind of string, and it'll just search for the entire title of a post. And I'll
see if the search keywords are anywhere in the post title, it doesn't have to match completely, it just
has
to be somewhere in the post. So this will give us a little bit of flexibility so that we don't
have to match the exact name of the post, we can just provide some keywords like, like hot dogs
or
hot dogs or beaches or whatever. And it should be able to just see if it's contained in there.
So let's take a look at our database. You can see that we've got a lot of ones that say top
beaches
in Florida. So I'm just going to do a search. And if I just search for beaches, it should
return all
of these because they all contain the word beaches. And I'm just going to do a search. And
I'll get
rid of the limits. And the skip for now, we don't really need that. And all of the
results
should be actually, I don't think I saved it. Let me let me save that. And now if I do
this,
you can see that every result is going to have the word beaches in in the title. And when it
comes to
query parameters, you can string as many as you want. So if you wanted the, the limit as well,
and we'll set the limit to two. And we can also provide a skip
equals one, you can add as many of the query parameters that you want. So now you can
see
that we set the limit to two and we skip the first one. Now the last thing I want to show you
guys is how to use spaces in your search query. Because right now, we just have the word beaches. But
let's
say I wanted, you know, just going into our Postgres database, let's see what I've got. Maybe
I want to search for beaches, hello, or something beaches, right? How do I how would I
search for that in a URL? I can't put a space in the URL. I can't say, you know, something
beaches.
So how do I do a space? Well, I can do percent and 20, which is reference, which means
space
in your URL. And then you can do beaches. So then just to verify that that's actually
returning what we want, I'm going to do a print, I'm going to print out the search
keyword.
And we're going to test this out. So if I hit send, you can see that it returned the one
result that
has the word something and then beaches, but it doesn't return anything else. What I would
like
to do now is do a little bit of cleanup on our main.py file, you'll see that there's a lot
of
unnecessary imports that we don't need, especially since we moved all of our routes into their
respective folders. So you'll see that a lot of these are grayed out. And so anything that's
grayed
out, we can move above. Before we actually do that, what I actually want to do first is I
want
to take this code for connecting to our database using the, you know, the regular Postgres
driver,
I'm going to move that to our database.py file. And keep in mind, we can just delete this
because we're no longer using this. And we're using SQL alchemy to actually connect to our database.
So
you can actually delete this. But just for documentation purposes, I'm actually going to just
cut this out. And I'm going to move this into our database.py file. Just that you guys have
this
for reference, in case you ever want to, you know, run raw SQL directly using this Postgres
library,
instead of using SQL alchemy, I want to make sure this course covers as many different, you
know, possible situations for you guys. But moving this here, obviously, we're going to get some
errors
because we have to import all of these. And I'm just going to go to our main and I'm going to
import them from here. So here, I'll just copy these two. And we'll actually just cut it
out.
And then it looks like we also need time. Where is that coming from?
It's just coming from the time. Okay. All right, that should remove any errors. And we can
just
comment this out actually, because we're not going to use that anymore. And then going back to
our
main, you can see that both of these are grayed out. So we're not even importing anything from
typing anymore, or we're not using anything from typing. So I'm going to remove that.
You can see that we're no longer using status. So actually, we're no longer using all of
these.
So I can remove all of those. And I'm not using the body here anymore. I'm not using
anything
from pedantic. And I'm not using anything from random. We'll just get that a little bit
closer.
We're not using session anymore either. So I'll remove that. This whatever that
is,
and I'll remove that. And then we'll remove schemas and utils. We can remove get DB as
well.
And then obviously, the the posts array or list that's no longer needed. These two
functions,
they're no longer needed. They were before we started working with databases. And
then,
I mean, we don't actually need this route anymore. But I'll just leave that in there. But you
can see that our code is a lot cleaner now, after removing all of those unnecessary
imports. I'm just going to structure this a little bit better. So all the codes a
little
bit closer together. And I think that's all the cleanup that we need to do for now. I just
wanted to make sure there wasn't too much garbage building up in our application. When we
first
started working on databases, I mentioned that we should never hard code our database
information,
or the database URL into our code. Because this creates two issues. First of all, we're
exposing our username and password right there in code. So anyone can see it. If we check it into
GitHub,
anyone who has access to it can see our password now. And at that point, we've compromised our
application. But on top of that, the other big issue is that right now, we've set this URL
to
go to our development Postgres instance, which is running on our local machine. When we
actually deploy this to production, our Postgres server is not going to be running on this
machine,
it's it may not even be running on the machine that our application is actually deployed on,
it could be on a completely different system. So we would need a way for the code to
automatically
update in a production environment to point to the actual production Postgres database instead
of using the one that's hard coded in here. And so we need a way to do that. And keep in mind
this,
this isn't exclusive to just working with the databases, any kind of confidential
information,
any password secrets, anything that needs to get updated, based off of the environment that
it's in
needs a way that will will run into the same exact issue. And if we go to our OAuth 2.
file,
you can see that we have our secret key hard coded in here. This is something that we don't
want to do. We don't want to expose this. And we need this to get updated between our
development and our production environment, because they may not be the same on both. So how
do we do that? Well, we're going to make use of something called environment variables. And so an
environment
variable is just a variable that you configure on a computer. Doesn't matter what operating
system,
they all support it. And when you configure an environment variable on your computer, any
application that's running on that computer will be able to access it. And that includes
our
fast API. So your Python app will be able to access any environment variables on the
machine.
And so instead of hard coding the values in our code, what we'll say is, hey, retrieve an
environment variable named, you know, whatever, like we might have an environment variable
called
Postgres underscore URL. And that's going to contain the URL of a Postgres database. And
so
we don't need to hard code the actual value, we hard code just the name of the variable, and
then Python will automatically pull in that, that variable. So let me show you guys how to
do
that. And I'm going to show you guys how to do this on Windows first. And then we'll take a
look at this on a Mac or Linux machine, Mac and Linux are the same steps. So I'm going to search
for
environment. And you'll see this edit the system environment variables. And what we want to do
is
we want to go to environment variables under the Advanced tab. And here you'll see two
sections,
you've got system variables, this is going to be environment variables that are system wide,
which means any user can access them. And then we have a user specific variable. So these
are
variables that only I can access. And you can just kind of, you know, go through these and
just take
a look at them. But you can see that we have an environment variable called path, don't worry
about what it's used for. This is something that gets set up when you install your machine.
But
we're going to take a look and try to access this. So you can see that this is the key, the
variable name path, and then the value is some sort of path in our operating system, or in our file
system.
So how do we actually access that? Well, let's open up a terminal.
And here, I'm just going to type in echo. And then we want to access an environment variable
just to percent and then the name of the variable. So I'll say path with a capital P.
And if I run this, you can see that it prints out that specific path. And so that's how you
access
an environment variable on the command line. With Python code, it's gonna be a little bit
different, but it's still pretty simple, I'm going to open up a new file, just for demonstration
purposes,
you don't have to actually follow along on this. And to actually access a environment
variable, we have to import the OS module. And then to access that variable, you say, OS dot get nv.
And
then within this method, you just pass in the name or the key of the variable. So in this
case, it's path. And I'm just going to store this in a variable, and we can call this
anything,
I'll just call this path, why not. And then I'll print out path so we can see how they see if
it
actually works. And now if I do pi dash three, and then run example.py, you can see that it
printed
out that same exact path. Now to set a new environment variable, we would go to
new.
And maybe I create a URL, a variable name called Postgres. Or we can just call this how about
my
underscore DB underscore URL. So this is going to have the URL to access our database. And
I'm
gonna say this is at localhost, colon 5432. I think that's what our database is running
on.
And I'll hit OK. And so here we can see that this is our new environment variable that we
created, my DB underscore URL, and it's set to localhost 5432. I'll hit OK. And then I'll hit OK
here.
And then what we're going to do is we're going to try to access that variable. So I'll do echo
my underscore DB underscore URL percent. And actually, it should be percent percent.
And you can see that it does not print out what we said, it just prints out exactly what we
wrote. So what gives what happened here? Well, anytime you set a new environment variable, what you
got
to do is you got to close out your terminal and open up a brand new one. So I can, you know, I
can close this out, or I can just open up a new one. And this new terminal, well,
actually,
I don't want a PowerShell, I want a command prompt. So this new terminal will be able to
access newly
created environment variables, because they get set as soon as the terminal opens. So once the
terminals open, I can't change them. So I have to open up a new one so that it can reread and
import
all of those environment variables. So now if I do echo, percent my underscore DB underscore
URL
percent. It looks like I may have mistyped this. So hold on, let me see what what is called.
And
that looks like it's correct. So what's what's happening? Well, I'm going to close out all of
them. That's probably what's the issue. So let me open up a new terminal.
Open up a new cramp command prompt. And now I'm going to try echo percent, my underscore
DB
underscore URL. And now we can see that we can read it. So we have to close out our
terminals
and open up a brand new one. It's kind of annoying. But that's just the way it works. Now on
top of
that, there's an even bigger issue. And I think this is exclusive to Windows. And that is
that
VS code, the terminals here, it's whenever you set a new environment variable, they never
have
access to it. So you could try deleting all of these terminals, and then creating a new
terminal. And then accessing that new environment variable of my DB underscore URL. And if I try to I try
to
access if I try to run this code, you'll see that it just says none. And if I just try to run
the
same echo command here, you'll see that it just spits it out. So it's not able to read it. And
it's
it's a little bit of an annoying issue with VS code in Windows, you have to close out every
single VS code window that you have and, and then reopen them. And sometimes even then that doesn't fix
the
issue. So know that this is just kind of a limitation with Windows. But ultimately,
don't
worry too much about that. Because you're going to see that especially in a development
environment, going into and actually setting all of these variables yourself is unnecessary, right?
I
don't want to have to go in and set one for my database, IP, my database port, as well as
my
secret key, my password, my username, it's just too many to manage yourself. And you'll see
that more complex applications could have 10 2030 environment variables that need to be
set.
So instead of going into your system and actually changing it, I'm going to show you guys in
the next video, I'm going to show you guys how we can get around this by using what's referred to as
an
environment file. And so you don't actually need to worry about the limitations we're running
here, because we're never actually going to set our own environment variables on our local
machine,
especially in our development machine, because it's just too slow of a process. And it makes
things unnecessarily difficult to troubleshoot. So before we wrap things up, if you want to
delete
that environment variable, feel free to do that, which we can just go here. And I can just
select that and then hit Delete. And let me get let me show you guys how to do this on a Mac. To set
an
environment variable on a Mac machine, the command that we run in the terminal, there isn't a
GUI kind of like in Windows, we actually have to use a terminal, which I actually prefer. What we do
is
we just type in the command export. And then we provide the name of the the name of the
variable
or the key. So I'll say my underscore DB underscore URL, then we say equals. And then we just
provide
the text or the value. So in this case, maybe this is localhost on 5432. All right, and then
to
actually read the environment variables that have been set, you type in print, and V. And here
we
can take a look at all the environment variables, most of these have been set by the machine
itself. And you'll see that nowhere in here, do we see? Well, actually, we do see my DB URL right
here.
However, just like I said, in Windows, sometimes, you know, depending on the terminal using
and a
few other things, sometimes you actually have to close out this terminal and open up a new one
to actually get the new environment variable. But here we can just access it
by typing echo. And then we can just say dollar, my underscore DB underscore
URL.
And you can see it prints it out. And then within Python, just like we did on the Windows
machine, accessing it is the same exact way it shouldn't matter depend, it shouldn't matter which
operating
system you're on, it's the same exact thing from your code's perspective. With any project,
there's
going to be a certain number of environment variables. And as I said, as your project grows,
you're going to have more and more of them. And one of the issues that you can run into is
that
you could potentially forget to set one of them, either on your development environment or
your production environment. And it's probably going to cause your application to crash. And so
what
would be good is to perform some sort of validation to ensure that all of the right
environment
variables have been set for your application to actually run properly. And on top of
that,
it's important to understand that when you read an environment variable, it's always going to
come out as a string, which which is fine, but it just means that you have to do all of the
validation
in code. So if you're setting an environment variable for your, you know, your access
token
expire minutes, right, we needed this to be an int, not a string, but when we read the
environment variable, it's going to come out as a string. So we have to convert this to an integer. So when
it
comes to verifying that all of the properties are set, as well as performing
validation,
we might have already kind of addressed this issue with a different part of our application.
Right. And that's our schemas, right? We do the same thing with all of the data we send in the
body
by using pedantic to perform all that verification to make sure that every single property
that we
need is there, and to also perform all of the typecasting. So if we set it something to be an
integer, it'll automatically convert it and validate that is a valid integer. And what's
great
about pedantic is we can actually use pedantic to perform all of that validation for our
environment
variables. And let's see how we can actually do that. So I'm going to go to main.py. And as
an
example, I'm just going to import from the pedantic library. Something called base
settings.
And so just like with, you know, any other pedantic model, right, we can set up a
class.
And I'm gonna call this settings. And this is going to extend base settings. And I didn't mean
to accidentally import base. And here, we can basically just provide a list
of all of the environment variables that we need set as properties on the class itself. And
so,
you know, maybe I need something called database underscore password. And this needs to be a
type
string. And we can even give it a default value, which is localhost. And then maybe I need
a
database username, which is going to be string and we can give it once again, a default
password,
we don't actually have to get a default value. But if you want to, you can and I'll say this
is going to be Postgres. And then maybe we want the secret key. And this is once again going
to
be a type string. And we can just give it, you know, some arbitrary default. And let's say
these
are the only environment variables that we need for our application to run. By setting it like
this, it's going to automatically perform all of the validation for us to ensure that all of
these
have been set. And an important thing to understand is that when you set environment
variables,
you know, if we just quickly go back to the environment variables on my
machine,
normally best practice for environment variables is to do all caps with underscores, right?
It
doesn't have to be all caps. But this is just kind of standard convention. You can see I've
got some that aren't like that. But in general, you want to do it all caps. And you can see here we
reference
it all in lowercase. So pedantic will automatically handle all of this from a case
insensitive
perspective. So it doesn't matter if you capitalize this or lowercase everything, it'll take
all of your environment variables and it'll convert it to lowercase just to simplify
everything. Then we create a variable called settings equals and then we say
settings.
So right here, all we're doing is we're creating an instance of the settings class. And so at
this point, pedantic will read all of these environment variables. And once again, it'll look for
them
from a case insensitive perspective. So it doesn't matter if they're capitalized or not. And
then it's going to and then it's going to perform all of the validation, it's going to ensure that
they're
properly a string and and so on. And then finally, it's going to store it in a variable
called
settings. So then we can access all of these variables by just saying settings dot and
then
the name of the property. So if I want to get the database password, I just do settings that
database password. And so we can print this out. And I'll just start my application. Right.
And
you can see that it printed out localhost. And if I want to grab maybe the
username,
I just say username. Actually, sorry, it should be database underscore
username.
And you can see it prints out Postgres, which is the default values because I haven't actually
set
my system environment variables to be any of these values. So it's going to use the default
one. And
like I said, we're going to run into that Windows issue where I can't exactly update it
without closing everything. So I'm not going to bother doing that. But what I will do just to show
you
how this validation works is let's remove this. Let's say there's no default password. And so
if
there's no default password, then you know, it's first going to check my system, my my system
or
user environment variables to see if there's something called database password. And since I
didn't provide a default one, if there is no environment variable with that name,
it's going to throw an error because the whole point of this is to validate that all of the
environment variables that we have are configured here. And so after I save it, if we take a
look
at this, you could see that we get one validation error for settings. So settings, database
password,
and we can see that it's missing. Alright, and so having that quick check makes our life so
much easier so that we don't have to figure out, you know, which environment variable is, is
missing,
which one's not, because like I said, you could have 10, 20, 30, 40 of them, there's going to
be a lot of them. And managing them does become a little bit of a challenge. Now, one quick
thing,
you know, let's say I go to my environment variables once more. And I've got
something
called path, right? So path is, you know, something, the path to a specific directory. And
what I'm
going to do is I'm going to say, I want to access the path. But we're going to say that this
needs to be of type int. So Python is going to perform some validation, it's going to read it and
every
environment variables always read as a string. And then it's going to try to type cast it into
an int. And if it fails, it's going to throw an error. And so since this is clearly not a
integer,
it should throw an error. So let's try this out. And we can see that value is not a valid
integer.
Now, for all of the settings in the environment variables, I usually like to create a separate
file to hold all of this information. So under app, I'm going to create a new file called
config.py.
And I'm going to move all of that code into that file. And then in our main.py file, I'll just
import from dot config import
settings. Alright, so ultimately, we're just importing that final class, the instance
of
the class that we created. Right. And so once again, it's able to perform the
validation,
it's going to report an error with the path variable. Alright, so having it in its
own
file is going to make things a little bit easier, because then we could just go to the
config.py file. And we'll know exactly, you know, where to look for all of our environment variables and
all
the configs related to our application. The last thing I want to do is, let's go ahead and
actually
figure out what environment variables we want to use moving forward, and actually set this up.
So we're going to clear out all of these. And I'm going to create one for all of the database
related
stuff. So database underscore host name is going to be a string by default. I'm going to
have
database underscore port. Once again, a string will have a database password. And then we'll
have
a database underscore name. So that's the database within Postgres that we're connecting to.
We'll need a username, all of these will be strings. And you may be wondering why the
port
is set to a string. But the reason why it's going to be set to a string is because if we
actually take a look at our database connection, it just goes into a URL. So we don't actually ever
need
to have it be an int, I guess you could make sure that it's a valid port number and
then
convert it back to a string. But that seems a little unnecessary at that point. Then
there's
a secret key for our or you know, our JSON web tokens. And we've got the
algorithm
for assigning our token. And then we have the access token, expire underscore minutes, which
is going to be an
int. Alright, and if we save all this, we should get a whole bunch of errors. And that's okay,
because none of these have been set. So at this point, you could technically, you
know,
whether you're on a Windows or Mac, set these on your machine and get everything to work. But
like I said, that is an unacceptable thing to do, because that's a lot of work. And we want
to
simplify things. So what we can actually do is within our fast API directory, we're going to
create a new file called dot ENV. So this is kind of standard convention. But this
file,
and don't forget the dot this file is going to contain all of our environment variables. So we
can set it much easier just by having a file do all of the work. Now in production,
you're not going to use this in product in production, you're going to actually set it on your
machine. But in development, it's perfectly okay to just set everything in here.
And so what we're going to do is we're going to take all of those environment variables that
we defined. And we're going to provide values. And so like I said, in real life, you generally
want
to capitalize everything. But luckily, pydantic automatically looks at everything from a case
insensitive perspective. And so our database right now is running on localhost. So we can just
say
localhost. Our database port is going to be 5432. The password, that's going to be whatever
you
chose to use as your password. It's gonna be password 123. And we've got our database
name.
Mine happens to be fast API, but yours is going to be something different probably. The
username, I'm guessing everyone's probably going to be using the default postgres. The
secret
key. I'm just going to grab whatever I used in my file. So I'll just grab
this.
The algorithm is going to be HS 256. And then the access token expiration
time
is going to be set to 30. We've got all the values set here. And one thing to note, I didn't
provide
any default values, and you could 100% go ahead and add default values into yours if you want
to, but I'm going to leave them as such. And now we have to tell pydantic to actually import it
from
our dot env file. And the way to do that is inside this class, we just say class config. And
we say
env underscore file, and then the path to our specific file. So we say dot env. So we'll
save
this. And I forgot to save my env file. And then I'm going to run this again. And our
applications
should start up with zero issues. And so hopefully you guys see how easy it is to work with
environment
variables, especially when you're allowed to set it on a file, so that we don't have to worry
about setting it either in the terminal or, you know, on a Windows machine having to go through the GUI
to
do that. But one thing to keep in mind is that you never want to check this env file into
Git.
Because this has all the credentials for your development environment, maybe you don't want
those to get out. And there's no need to ever check it into Git. So if you create a git
ignore
file, so it would just dot git ignore, you want to make sure that you put in dot env. And
we'll
come back to the git ignore file when we start working with Git in this course. But you also
want to make sure you remove anything with underscore underscore pycache underscore
underscore,
which is all of these, as well as your virtual environment folder. So you want to make sure
none of those get checked into Git so they don't get uploaded to your GitHub repo. And just to
make
sure that everything's working, let's go ahead and just test this out real quick just to make
sure that nothing's broken. And that seems to work. And then if we try to get all posts,
everything
seems to be working. So we have now successfully moved everything over to environment
variables.
And actually, sorry, we forgot to do one very important thing. We're not actually using
the
environment variables right now. Because if I actually go to my database, you can see
that
it's still just a hard coded value. So we have to make sure we actually update that to use the
environment variables. So we're going to change this to be an F, an F string. And then
here,
this is going to be the username, this is the password, this is the host name, and then this
is the database name. And then there's also going to be the port number, which would be right
here,
which is 5432. I believe that's Yeah, and that's gonna be the port number. So what we have to
do is, since we're using an F string, I can just remove this. And I can say, well, first of
all,
we have to import our config file. But we have access to it. Or we can do from config
import
settings. Just put a dot there. And now I can just say settings dot database. And then we'll
grab the
username here. And then for the password, I'm going to do the same thing, I'll
say
settings dot database password. And the host name. This is going to be settings dot database
host
name. And then there's going to be the port number. And then finally, we've got the actual
database
name. I think everything should still work. Just to test this part, let's just quickly log in
now.
All right, everything still works. So we access to our database is now working just fine.
But
there's a few other things that I want to update. And keep in mind, if you're using the
Postgres
driver to actually connect and make queries, then you would just update it here with the
settings dot whatever. But if we go to our OAuth two, we can all first of all, let's import our
config.
And we'll change the secret key. This is going to be settings dot
secret key, not schema secret key. The algorithm is going to be settings dot
algorithm.
And then finally, this is going to be settings dot access token expire
minutes.
All right, let's reload this. Let's just double check a few things. So let's, let's, well,
let's create a new user. All right, we were able to create a user, let's log in that
user to log in, and then let's get posts. And we're able to get posts. And let's just
double
check that there's no errors. So everything looks good. And so now we have successfully
migrated
all of our code to using environment variables. And so when we move to production, we can
just
on the machine, set all of these values that we have inside our config.py file, and it's
going
to automatically import it, and then update those values wherever we reference them. In any
social
media type app, there's going to be some sort of voting or likes system. So Facebook has
likes,
Reddit has up votes and down votes. Instagram has likes, Twitter has likes as well. And so
we're
going to implement a simple like system as well. And we're going to quickly go over the
requirements for it. So the first thing is a user should be able to like a post, a user should only be
able
to like a post once we shouldn't be able to like a post 10 times and then artificially cause
the
number of likes for that post to go up. And then finally, anytime we retrieve a post from
our
database, or from our API, we should also fetch the total number of likes for that post. Now
let's
take a look at the requirements for our voting model or like model. And so naturally, just
like
we have a table for users and a table for posts, it makes sense to store the votes in another
table,
just like we always do. And if you think about the requirements or what we should do for
the
columns, well, we need to have a column that's going to store the ID of the post that
we're
ultimately going to like. And then we're also going to need a column that references the
idea
of the user who liked the post. So those are the two absolute minimum number of columns that
we
need to get our voting system in place. Keep in mind if you wanted to do a you know, an up
vote
down vote type thing like Reddit does, then you might want to have a third column for the
direction of your vote. But we're going to keep this nice and simple. And it's just going to be a very
simple
like system. So it's just one direction. But the most important thing of the votes table is
that
since a user should only be able to like a post once, this means that we need to ensure that
every
entry, every post ID and voter ID is a unique combination. So what do I mean by that?
Well,
take a look at this, we have a post with an ID of 12. And this post was liked by a user ID of
four.
So this is what an entry would look like. And that's perfectly fine. And if we go and we
here
we've got a post ID of 28. And it looks like a user with an ID of nine like this post. And if
I go all the way down to here, you'll see that post ID of 12, which you can see that there was
already
a row with the post title is liked by a different user nine. So it's perfectly okay to see a
repeat
in this column. And under the user ID section, it's perfectly okay to see a repeat in
this
column as well, because we can see that a user of ID nine, voted and liked a post with an ID
of 28,
as well as a post with an ID of 12. And then obviously, one post can be liked by
different
users. So a post with an ID of 12 was liked by a user with an ID of four, and a post with the
same
post was also liked by a user with an ID of nine. Now, what we can't have is a user liking a
post
more than once. So you could see here, user to like to a post with an ID of 55. We can't have
him
do that again, right? It's a duplicate, this isn't allowed, or this shouldn't be allowed in
our system,
because we don't want users to be able to like like a post more than once. And so there's a
couple
of different ways of setting up this requirement. But we're going to take a look at the
simplest solution. And we're going to learn about something called composite keys. So we've already
covered
what a primary key is, it's a it's a column in your table, that's going to ensure that every
single entry is unique. And we always used a column called ID, which had a auto
incrementing
integer. However, what we can also do is make use of something called composite keys. And a
composite
key is nothing more than a primary key that spans multiple columns. So we've only worked with
one
column primary keys, but we can actually have it cover more than one column, we can have two
columns
or three columns. And since a primary key must be unique, this will ultimately ensure that no
user
can like a post twice, if we make sure that both of these columns are part of the primary key.
Right. And so when you have a composite primary key, it does not care if there's duplicates
in
one row, it does not care if sorry, in one column, and it does not care if there's duplicates
in the other column, all it cares about is, are both of the columns, the same in two different
rows.
So once again, you know, for a post of ID 12, we have a user for who likes it, as well as a
user
of nine that likes it. And so with a composite primary key, it sees that as two different
entries, because it needs both of them to be the same to be considered a duplicate. So we
can
uniquely identify this row by saying, hey, I want the row with the post ID of 12. And a post
ID of
nine, there's going to be no other rows with that combination. And the same thing goes in the
other direction. The user nine can like post 28. And he can also like post 12. And so once again, we
can
uniquely identify either one of these rows, because we can say, hey, post 28 with the user ID,
that's
a unique combination and a post ID of 12. With a user ID of nine, once again, a unique
combination.
However, we can't have a user with ID of two like 55. And then once again, the user ID of
two,
like 55, because the combination of these two is now a duplicate. Because one is 55. And
two,
and the other one's 55. And two. And so that's all a composite key is it's just a primary key
that spans two columns and ensures that across both columns, we have unique combinations for
things.
So let's figure out how to create our new votes table. And I'm going to first show you guys
how to do this within PG admin. So just with regular Postgres, and then we'll take a look at how
to
do this with SQL alchemy. And I'm just going to go under tables, and I'm going to create a new
table,
we'll call it votes. And then under columns here, I'm going to add a column. And this is going
to be
post underscore ID. So this is going to be the column that referenced the ID of the specific
post. This should and then actually before we do anything else, I'm going to add the other
column
in as well. And then we're gonna have the user underscore ID. And so these are both going to
be integers. And then we can just select primary key primary key. And that's going to create
that
composite key. And then we have to set up the foreign keys as well. So these are going to
reference other tables. So for the post ID, we can go into constraints. And then we'll go into
foreign
key, we'll add a new foreign key. And I'm going to call this votes underscore posts
underscore
primary whoops, not primary key foreign key. And actually, sorry, I didn't mean to do
that.
And then we're just going to go into here. For the columns, we're going to select the
local
column, which is going to be post ID. Sorry, it's not post ID, yeah, post ID. And then for
the
referencing table, it's going to be the posts table. And then we're going to grab the ID of
that.
And for action on delete, we're going to make sure it's set to cascade so that if the you
if
the post gets deleted, we're gonna delete this entry. And then we'll add that in there.
Oh,
it looks like it's got deleted. Let me just redo this. We'll add that. And so we've got that
in
there now. And that should be it for the first foreign key, let's add another foreign key.
And
this is going to be called votes underscore users underscore foreign key columns, this is
going to
be user underscore ID of the local table, this is going to reference the users table, and
then
it's going to reference the ID. We'll hit that actions. This is going to be cascade as
well.
And we'll save that. And then I'm going to right click on the votes table. And then we can go
to view edit data, we'll go to all rows. You can see it's a very simple table. So let's create a
new
let's create a new vote first. And so I'm going to first of all, I've got way too many of
these
windows, I'm going to just quickly delete those. Alright, so I got those deleted, I'm going to
open up a new query. I can just go into database, we'll do query. I'm going to do select star
from
posts. And then select star from users, just so I can get a list of post IDs and user
IDs.
So I got user ID of 212324. And for the post, we got 1049. So let's try those
out.
And I already forgot them. So let's get let's get that post ID again. So we'll just grab a
post ID of 10. Oh, there's going to be 10. And then the user ID. Well, I'm going to open up a new
window
so I can just switch between them. So now we have both of these windows for querying. And I
can do
select star from users. 21. Okay, so we'll do. And it looks like we hit a bug in PG
admin,
because I can't write anything here. So I'm just going to do view added data all rows again.
And that looks a little bit better. So let's get a post 10 and a user of 21 of 10.
21. And let's see if we're successfully able to get that and looks like that worked. And then
if
I grab a post ID that doesn't exist. So let's say like 99. And then a valid user. Let's see
what
happens. It's going to throw an error. That's good. And if we grab a valid post number, but
a
idea of a user that doesn't exist, we should also get an error. And then if any of these are
blank,
it save it should throw an error as well. Okay, so our table looks set. That's pretty much all
we have to do. There's nothing else for these tables. You'll see that once you can create one
table,
you can really create a mall. So I'm just going to drop this table for now. And in the next
video, we'll take a look at defining our model and SQL alchemy. So that SQL
alchemy actually generates the table for us. In our models.py file, let's create our model
for
our votes table. And we'll call it votes or just vote. It's going to extend
base.
And then for our table name, I think it just makes sense to call it votes. Then let's set up
our two
columns. So we're gonna have a user ID. And then we're gonna have a post underscore ID. So for
the
user underscore ID, it's gonna be a column has to be of type integer. We're gonna set up the
foreign
key. And this will be users. So it's going to reference the users table and grab the ID
field.
And then the second property is going to be the on delete, which is going to be set to
cascade. All right, then we'll set the primary key to be true. And that's pretty much it. And then
I'm
just going to copy this. Paste it here, we're just going to change this to be instead of users
ID,
we're going to be going to be posts dot ID. And I don't know why I got auto formatted to a
different
line. Okay, okay, it looks like that's what it prefers. And so save this. And assuming
there's
no errors, our application should restart. And then if we go to PG admin, and then
refresh,
we should see a votes table. And I'm just gonna go and take a look at the
properties.
And you can see that we've got the user ID and the post ID, we've got the primary key set. So
it's a composite key. And since it's a primary key, not null is going to be set to yes, we go to
constraints.
We've got, you know, once again, both the primary keys, we should have two foreign keys. And
then if we just take a look at one of them real quick, we should see cascade on
delete.
And so if we go and view and edit data, let's test this out 1021 again. So it should be 21 and
10,
because the user IDs the first column now 2110. Let's try saving that. Good. Let's try doing
21
and nine are 21 and 88 should error out perfect. And then let's try 10 and some random
number
error perfect. Now there's going to be a couple things that we need to take into
consideration
when setting up the vote route. So first of all, what is the path we're going to use? And I
think it makes sense just to set up a new path called slash vote, just like we have for slash users
when
it comes to handling anything with users. And we have slash post for handling anything with
post, we're going to have the slash vote for handling voting. Now the user ID, the user who's
trying
to vote on or like a post, the ID of that user is going to be extracted from the JWT token. So
we
don't actually have to extract that from the body, we don't need to include in the body.
However, the body itself is going to contain two pieces of information, the ID of the post we're trying
to
like, as well as the direction of the vote. And what I mean by direction is, do they want to
vote
on the post? Or do they want to remove a post? Because like any application, you know, maybe
you accidentally clicked on a post to like it, but you realize you don't want to like it. So you
click
on it again, to remove that. So vote direction of one means that we're going to like the
post,
and then a vote direction of zero means we're going to remove our like of the post. Now
in
our routers folder, we're going to create a new file, which is just going to be vote.py. So
this is going to handle all the routing for our voting URLs. And what I want to do is I'm just going
to
copy the import statement from one of the other routers. And we'll set up a route. So let's
set
up the decorator first, and this is going to be a post. And I actually have to import
router.
And we got it, let's create our router instance. So we'll say router equals API
router.
And we'll set up the prefix to be slash vote. And the tags, we're going to give it its own
section, which is going to just say,
vote. And so now we can do at router dot post, this is gonna be a post
operation,
because we have to send some information to the server. And the URL is just going to be
slash,
so it's just going to be slash vote, then. And since we're going to create a vote, by
default,
we're going to send a different status code, we're going to set a 201 instead of the default
200.
Then we'll define our function, which I'll just call vote. Now, there's going to be a couple
things that we have to pass in. So since we expect the user to
provide some data in the body, it usually means we want to define a schema just so that we
can
ensure they send us the exact information. So let's set up our schema for voting. We'll
create
a class called vote. And so the first field should be a post ID, which is going to be a type
int.
And then we're going to have a direction, which is going to be an integer as well. So it's
either
zero or one. However, I would like to be able to validate to ensure that it's only zero or
one,
I couldn't exactly figure out if there was a way to do that in pedantic. But one thing we can
do is we can use content. So we'll import that from pedantic automatically. So if you want to see
the
import, that's what it's going to look like. And we can say less than or equal to one. So
anything
less than one is going to be allowed. The only problem with this is that allows for negative
numbers. But that's okay. You know, if you guys figure out how to just specify zero on
one,
then you know, go ahead and replace that and definitely leave it leave that piece of
information in the comments so that anyone else who wants to set up that specific restriction, then we all
know
how to do that moving forward. And let's import our schema now. So we'll go up a
directory.
And let's also import database models. And we're going to have to require the user to be
logged in
before they can vote on something. So let's import a walk to until here, we're going to set up
the
schema. And we're also going to set up the database so that we can make queries. And then
lastly,
we'll get the current user. So once we get all of those dependencies, it looks like we have
to
import session as well. And I'm actually going to move this right below the first import, not
that
it matters, but so we're going to set up logic for when the vote direction is one. So if I
if
vote dot der, so we're pulling the direction from the vote schema equals equals one, then
we're
going to perform some logic. Else, if it's zero, then we're going to perform some other
logic.
So if we want to create a vote, the first thing we're going to do is query to see, you
know,
does the vote already exist. And so what I'm gonna do is I'm gonna say db dot
query.
And we'll grab models dot vote. And I'm going to filter based off of models dot
vote,
dot post underscore ID. And we're going to say it equals equals vote dot post underscore ID.
So
we're going to see if there's already a vote for this specific post ID. However, this isn't
enough, because remember, multiple people can vote on the same post. So we actually have to do a
second
check. And we can add in a second condition by just doing a comma and then providing the
second
condition. So we have to say models dot vote dot user ID equals equals current user dot
ID.
And I'm going to save this. And we're going to call this vote underscore query. So this isn't
going to actually query the database yet. This is just building up the query. And it's going
to
check to see if this specific user ultimately has voted for this specific post already or like
this
post. And I'm actually going to move this above the if statement. And you guys will see why
we're
going to do that in a bit. And we're going to then perform the query and just save
that
result under found vote equals vote underscore query dot first. All right. And so if the
user
wants to like a post, but we already found a post, that means he's already liked this specific
post,
so he can't like it again. So we're going to say is if we already found a vote, then we're
going
to raise an HTTP exception. Now the status code, we're going to use a new status code, I'm
going to
use status dot and then this is going to be a 409 for conflict. And there's other status codes
you
could potentially use, I just decided that this is probably going to be the best fit. And then
for detail, we're going to just pass an F string and say user, and then we pass in the user, which
is
a get current underscore user dot ID has already voted on post with an ID of vote dot post
underscore
ID. Alright, but if we didn't find a vote, then what we're going to do is we're going to
create
a brand new vote. So I'll say models dot vote. And so the post ID field is going to be set to
vote
dot post underscore ID. And then we can grab the user ID field from current user dot
ID.
So this will set the two properties. And we'll save this as new underscore
vote.
And as usual, to actually perform these changes, or to add this to our database, we have to do
DB dot add new underscore vote. And then a DB dot commit. And we don't actually need to send
the
created vote back to the user because it doesn't really provide any useful information. We're
just going to send a message that says successfully added vote. Now, if the user provided a
direction
of zero, that means they want to delete a pre existing vote. So first of all, we'll say if not
found vote, right, we can't delete a vote that doesn't exist. So we'll raise an HTTP
exception.
Status code, I think we could just send a 404.
And for the detail, just say vote does not exist. But if we did find a vote, then we have to
delete
it. So I'll say vote underscore query. Remember, this is a query all the way at the
top.
I'll say dot delete. And then we can just do our synchronize, synchronize session false. And
we'll
do a DB commit. And we'll just return a similar message successfully deleted vote. Okay, and
this
should kind of sum up all of the logic in our path operation function. And at this point, we
can just
go ahead and test this out. So we'll go back to postman. First of all, let's log in.
Alright,
so we've logged in. And what I'm going to do is create a brand new request, we'll call this
vote. And this is going to be a vote. And in the body, we have to pass in the specific data.
So
let's actually grab a post or an ID of a post. So in this case, there's an ID of
10.
Actually, first, let me double check who I'm logged in as. Okay, that's fine. I'm gonna
grab
a post with an ID of 10. Here, we'll say post underscore ID 10. And then we'll say, der,
actually,
is that what we called it? I actually forgot to go to our schemas. Yeah, it's just
der.
And this is going to be, let's, let's, this is gonna be a one so that we can actually like the
post. And remember, this should be a post request. So let's try this out. Let's hit
send.
And it looks like we ran into an issue. So let's check. And it says that this specific route
was not found. And so that's obviously because we didn't
wire up this specific router. So let's go back to our main.py. And let's import vote. And then
we
can just copy this and wire up vote router. Yep. And once again, another typo. All right,
let's give
this a try now. So we'll hit send. It says we're not authenticated. So let me log in user.
All
right, now let's try this. And it says that I'm still unauthenticated. And guys, I realized
why
we're getting not authenticated, that's just because we have a new request. And we have to
make sure that we set the authorization to be bare token. And then we want to pass in our
JWT
variable. So it wasn't actually retrieving that. So now if I try this, we can see that we
successfully
added vote. And, you know, just make sure that it actually did that, let's actually go into
our
database and take a look at our votes table. And so we like to post and it looks like I have
some
previous ones. So what I'm actually going to do is I'm going to delete everything from our
votes table, just so we can make sure it works. Because I forgot who I'm even logged in as let's go
to
properties. Sorry, let me just do a query. I'm just gonna say delete from votes. That's
going
to delete everything in our votes table. Perfect. And then we can just say select from
votes.
We have nothing in there. And let's try this again. All right. And so now run this, we
can
see that we did like post 10. And I guess our ID is 24. But we can just quickly verify that I
do
select star from users. And I'm logged in as Sanjeev 123. So we can see that that has an
ID
of 24. So that works. Now what I'm going to do is let's test to see what happens if I try to
vote
on a post that I've already voted on. So I'm going to keep the vote direction to be one. And I
know
I've already voted on this post. So we should get user 24 has already voted on post 10.
Perfect.
Now let's go and try to delete a post. I'm gonna set the vote there to be zero. And let's see
what
happens. It says I successfully deleted the post or the vote. And let's just do a quick
query,
we can see nothing is there. And then if we try to delete a vote, or a like that doesn't
exist,
we can see that vote does not exist. All right, guys, we've actually implemented all of the
voting logic right there. The only other thing that we need to do is when we retrieve a
post,
right, whether it's through the get all posts, or even I get one post, I want to have my fast
API
send the number of votes as a property as a field here so that, you know, when we load up
posts on
our front end application, they can see the number of likes that a post has. That's usually
how most
applications work. And I would like it to have it automatically send that information instead
of having to send another query to our back end to see, you know, what is the total number of
votes,
I wanted to do it automatically, anytime we retrieve any post. And that's going to be
a
little bit more complex, we're going to have to learn a little bit more about SQL, start
digging
into some of the weeds of SQL and Postgres. And then once we learn how to do it with regular
SQL,
we have to see if we can do this with SQL alchemy. After I finished recording all of the
voting logic,
or at least the router that handles voting, I realized there's one tiny bug. And that is
that
right now, there's no logic to actually detect if the user is trying to vote on a post that
doesn't exist. Because if they want to vote on a post that doesn't exist, I want to ideally send a
404.
So what we're going to do is we're going to implement that logic. And it's going to be really
simple, we're just going to query for the post based off of the ID. First, if it doesn't
exist,
we're going to throw an exception. So just like we've done with pretty much all of our other
routers, whenever someone tries to act as a post that they don't, that doesn't exist, then
we're
just going to simply send a 404. So let's start off by making our query, I'm going to do DB
query,
we'll say models dot post. And then what we're going to do is we're going to filter based off
of
the post ID. So we'll say models dot post dot ID equals equals vote dot post underscore ID.
And
then we're going to grab the first one. And we'll save this in a variable called
post.
Then we'll say if not post, so if the post doesn't exist, then we're going to raise an HTTP
exception.
And the status code is going to be a 404. And the detail will be the usual f string that says
post with ID, and then we'll pass in the ID.
Does not exist. And that's all we have to do. And that's going to fix that little bit
that
little tiny bug. And you'll see that when we get to the testing section, we can now test to
see if the post doesn't exist as well. Up till now we've been working with very simple queries, we've
just
been querying a single table. However, now that we have more than one table, and all of these
tables
have built out these relationships, you're going to see that eventually you want to be able to
get information from two tables at the same exact time. And this gets a little complicated. But the
way
we actually do that is by using something called a join. So we join two tables together. And
as an
example, let's say I want to get all of my posts. So I do a select star from posts. That's
great and
all. However, you know, when we're sending back the the posts to the user, right, the or our
front
end, our front end has no idea what a owner ID of 23 is, he doesn't know what that is. And we
ultimately want to display the username or the email of the user that created the post on
our
front end. So what we would have to do if we could only, you know, retrieve data from one
table at a time is we'd have to then individually go and then fetch all of these users from the user
table,
one by one, so that we can actually get the email or the username. And that's a
little
inefficient having to do multiple queries. If you ever get a chance to, it's better to just do
a
single query and get all of the pieces of information that you're interested in. So I'm going
to show you guys how we can join information from two tables, because I would also like to get all
of
the user information based off of the owner ID field that's on the posts table. So how do we
do
that? Well, we start off a query just like this. And actually, before we go into this, what I
would
like to do is show you one of my favorite websites. So if you go to Postgres tutorial, just
search
for that in Google, you'll see that there's a website called Postgres, q l
tutorial.com.
This is one of my favorite sites. And I want you to go to joins right here. So this is how we
get
information from two different tables. And this, this page, you do does an absolute marvelous
job
of explaining how to actually join two tables. And it goes over the different types of joins,
because there isn't just one method, you've got left joins, you have an inner joins, you got
outer
joins, you got right joins. And I strongly suggest you guys quickly just kind of read through
this
just to get an idea. Because I don't want to cover every single join, I'm just going to show
you the ones that we need to get the data that we're interested in. So let's go back to
Postgres.
And let's create our first join. So here, I'm do select, I'm just grabbing all of the fields.
And I'm going to grab it from the post table. And what we can do is I can say left
join.
And then we specify the other table, we want to jam into my query. So now I want to get data
from
the users table. So I do users. And then we have to find a field in both tables that we need
to
match on or join the table on. And since we want to find the specific user associated with
the
person who created this post, we need to join on the owner ID. So what we say is on so this is
how
we specify what it should join on. And we want to say, we grabbed the first table's name,
right,
the posts table, we grab posts. And then we say dot and then we grab the specific column we
want
to join on. So owner underscore ID. And then we just say it equals two. And then we grab
the
second table, which is called users. And then we grab the specific column from the users table
we
want to join on, which is users dot ID. So now if I run this, take a look at that, it took all
of
the user data. And it jammed it right into one single query with the posts. And so now I get
the
entire post. And I also get information about the specific user because we joined on owner ID
and
the user ID together. So it was able to kind of take that information from the other table and
match it right up to here. Because we did a star, it's going to grab the columns from it's going
to
grab every single column from every single from both of the tables. And there may be times
where maybe you don't want that maybe we just want a couple of columns. So let's say we want the
title
column. Maybe we want content. And maybe we want email. And this should allow us to grab the
title,
which comes from the post table, and the content, which comes from the post table and then
email,
which comes from the user table. So let's see what happens when I run this. Right, you can see
I got the title, content and email. Now, if I remove this and go back to everything, just real
quick,
let's say I want to get the post ID and the email. Well, let's see what happens. I'm going to
do
ID. And then email. Well, it looks like we have an error. It says column reference ID is
ambiguous.
So what exactly is happening here? Well, I'm going to remove this and show you exactly what
happened.
The problem is, is that the post table has a column called ID. And the user table has
a
column called ID. So when I just say ID, it has no idea which column I want. So that's why
it's saying it's ambiguous. So what we need to do is anytime there's potential for
confusion,
we always have to specify the table name before. So I can say posts dot ID. And then I could
say
email. Now, I don't have to say, you know, user dot email, because only the users table has
an
email column. However, if there was an email column in the post table as well, then we would
have to append the table name first. So you only need to do this when there's confusion.
However,
if you want to just kind of stick to a certain convention, you can just set up every single
property to have the table name beforehand, so that you know which table it's coming from
makes
it a little bit easier to read. And so now I got just the ID and the email. Now, let's say I
want
to get every field from one of the tables, I can say posts dot star, that's gonna grab every
field.
So it's grabbing the post table. And then the star means hey, grab every single column. And so
I can do that, it's gonna grab everything from the post table. And then I can grab maybe just the
email
in this case. And so this kind of ultimately does what we had wanted it to do. Because you
know,
I told you guys, we want to be able to get the email or the username information for each
and
every single post. And if we want to, you know, we can add in some of the other fields, maybe
the
maybe the user dot ID. So the user ID, however, that's just going to match up with the owner
ID. But just to show you guys, there is some flexibility. And as I mentioned before,
there are different types of joins, there's left join, there's right join, inner join, outer
join, I don't want to spend too much on it. But I do want to cover just a few things,
or it's the direction references which table. So right now we're working with two tables.
There's
the first table that's referenced by from and then the name of the table. And then there's the
second
table, which is referenced after the join, which is right here. And so the first table that's
referenced is always referred to as the left table. And the ref, the table on the
right,
the second table that's mentioned is always going to be the right table. So you got the left
table
and the right table. And so when we do join, what it's actually doing is, well, I think this
page
actually gives us the best explanation. So a left join, select data from the left table. And I
don't
know if this is big enough for you guys, but I'll zoom in. So it's going to select data from
the left table, it's going to compare the values between the two columns that we have
selected.
And for us, in this case, it's going to be a post dot owner ID on the left side, and then
users dot
ID on the right side. And it's going to find where those values match. And if they are
equal,
the left one creates a new row that contains the columns of both tables and adds this row to
the result set. And what's important is in case the value does not equal, the left join also
creates
a new row that contains the column from both tables and adds it to the result set. However, on
the right side, the the value is going to be set to null. So if we ever had a post
that didn't have an owner ID, then what would happen is we would see the the user
information
all set to null in that case, or the email would be set to null. However, since the way we set
up our table is that every single post must have a owner ID, we're never going to run into
that
situation. Now before we move any further, just to show you guys what a right join is, it's
the exact same thing, but in the opposite direction. And so let's take a look.
And things look a little bit different now. Because what's happening is that we're going
to
is that we're going to go through the two tables and find if the owner and the owner ID and
the ID of the user match. And we're going to join it on that. And we're going to do this for
every
single entry. And then at the end, you can see that user 24 has no information on the left
side.
And that's because user 24 didn't create any post. So the right join will show you instances
where something exists in the right table, but it doesn't exist on the left table, the left join will
show
you something that exists in the left table, but doesn't necessarily exist on the right table.
But
I'm going to change this back to left join. And now what I would like to do is it would be
great
if we can get the number of posts by each user, right? How many posts has each user
created?
So is there a way to do that? Well, there is actually. So what I'm going to do here
is
I'm going to grab the users dot email, actually users dot ID, and maybe we want the users
dot
email as well. Actually, we'll just get the ID. And here at the end, I'm going to say I want
to group
by users dot ID. So we're going to find all of these entries in our join, and then group
them
based off of the user ID field from the user column. And so once we group them, we can
then
actually count up all of the columns, sorry, all of the entries on a per user basis. So if I
say
user ID, and then we say, we can use a built in postgres function. And here, if I put in
star,
and I'll come back and explain what this means, we can hit run. And we can see that the user
with an ID of 21 has 11 posts, and the user with an ID of 23 has two posts. The only issue is, if
I
actually do another query right here, I do select star from users. There's actually three
users,
right? And one of them hasn't made any posts. And so that's why we don't see it in that list.
And if we want to be able to see it in that list, we need to actually change our
query.
And like I mentioned, the right join, if I actually remove the account column right
here,
and we just do a star, this is going to actually list every single user, even if the user
has
no posts. And so if I remove the group by ID real quick, right, you could see down
here,
I have user 24 with no posts, that's exactly what we want. And so now I can say group
by
users dot ID. And then here, I'm going to say select users dot ID, and then we grab the
account.
So let's see if this works. And notice how all of the post fields are completely empty. So
that's,
that's perfectly fine. And then we can run this. And you can see that it seems to have
worked,
right? User 21 has 11 posts, user 23 has two posts, and then we got user 24. But it says
that
there's a value of one. And the reason for that is that when you do star right
here,
that means it's going to count null entries. So since we have that one null entry, it's going
to count that as one. So instead, what we're going to do is we can actually pass in the name of
a
column. And so once again, I'm actually going to copy this, paste it into here, we're going
to
remove the group by and then just do a star. So we're going to get every entry, except
because
we did count star, it always ends up counting this as one. So you that user 24 is always going
to
have one entry, but that's not accurate, because he doesn't actually have any. So instead of
doing count star, we can actually pass in a column that we want to count. And so in this
case,
we can pick any of the posts column that we're trying to retrieve. And so I can just
say,
post dot ID. And so when you pass in a column name, it does not count columns that have a
null
value. So if the user doesn't have any posts, he won't have anything under post dot ID. And
then
if I just remove this query, and then run this, you can see that now I have successfully
gotten
the exact query that I want. And I can rename this column as user underscore post
underscore
count, or something. That's a little bit more readable. And then you know, I can grab any
other field that I want, maybe I also want the users dot email. And so that way, we get
just
the information that we ultimately want. So you can see that you're doing these queries, they
do get a little bit more complex. And you have to practice this a lot. And so I
definitely
recommend you actually just read through this document, set up a few example tables and just
practice doing your queries. But we're going to spend just a little bit more time on
these
queries so that we can figure out how to get the total number of votes. It is a little bit
longer of a process. But I want to make sure that you guys understand exactly what we're
doing.
Alright guys, so let's move on to working with our posts table and our votes table. So I'm
going
to do a join on those two. So let's start off by doing select. And then we'll just do star for
now.
And we'll say select star from and then in this case posts. And keep in mind, guys, I
mentioned
that the first table you reference is the left table and the second table that you reference
is the right table. It doesn't actually matter which one is the left or right. The only thing
that
matters is that you set up the right direction of the join. So if we set up posts as the left
table,
and we want to do a left join, then if we ever rewrite our SQL query and set up
our,
our votes table to be the left table instead, then we would just have to flip the join to be a
right join. So it doesn't matter which table comes first, keep that in mind. So you
don't
have to start your query on just the posts, you could do this on votes as well. And you just
change the direction of the join. But I like to do posts. And then we're gonna try a left join
once
again. And we'll say that the other tables can be votes and we're going to join on
well,
if we take a look at our votes table, which I'm actually just going to cut this out real
quick
and save that in my scratchpad. I'm going to just say select star from votes. We'll run that.
Right,
so there's going to be a user ID and a post ID. And then if I do a select star from
posts,
we have all these fields. So I think you guys can guess the exact column that we need to join
on.
So we need to join on post ID, and then votes dot dot post underscore ID. So whenever those
match,
we need to get a result. So I'm going to go into my application and just run a few. I'm just
going
to vote on a few posts, so that we actually have some data. So I'm logged in as a user, and
I'm going to vote on post 10. All right, we successfully added votes, I'm going to get a
whole
bunch of other posts. I'm going to vote on post four, and nine. All right, and I'm going to
log
in as a different user now. Let's see who else I got. All right, I'll just log in as Sanjeev,
I guess
my login. All right, we've logged in. And I'm going to vote on post 10 as well on this
guy.
And then we'll grab some random vote 15. Okay, and so we should have a decent number of votes
now.
And I purposely made it so that one of the users didn't vote, just so that we can take a look
at
what complications that add. Alright, so we have all of these votes. And we've got the user ID
and
the post ID. So now let's set up a join. So I'm going to do select star from posts. And I'm
going
to start off with the left join. And we'll say we want to join with the votes table. And the
field
that we're going to join on is on the posts, we're gonna grab the dot ID, and the and wherever
this
is equal to votes dot post underscore ID, we want to join the table. So let's run this. And
let's
see what results we get back. Alright, and so you can see that hey, look, a post with an ID of
10.
Looks like it does have a vote. So this is one join right here. But then you'll notice that
I
if I go down, you'll see another entry. So this doesn't mean in our database that we have to
post with an ID of 10 is just saying that since we join them, we have to go down the list, see where
they
match, and then print out a row. So it's going to print out a row for every single time, the
post ID
matches the post underscore ID of the votes table. And so since we have two votes for the same
exact
post, that means we're going to get two different entries doesn't mean there's actually two
posts in our table is just the way that the joins work. And I strongly recommend like if this is still
a
little confusing, keep reading that document and play around with it. If you want to see the
opposite,
actually, before we take a look, let's just take a look at a few other things. You can see
we've got post with four. And we can see that there's one vote for that because
there's
no other entries here with an ID of four. And then we could see all of these posts that
have
no votes whatsoever. So that's what the left join gives us because anytime we have a
post,
which is the left table, since we do a left join, it's going to list those posts. And I'll
just put it all on the right side. Whereas if we change this to a right join, it's going to do the exact
opposite.
With the right join, right, once again, it's going to look for any time the the ID here
matches the
post ID, and it's going to spit out a row. And then the same thing goes for the post with an
idea for and then once again, you can see 10 shows up twice because two users voted on
it,
and then 15 and so on, but then you'll see nothing else. And the reason for that is, we did a
right join. So it's going to show us all of the votes and their corresponding post.
However, it's also going to list out any votes, because it's a it's a right join that
don't,
that aren't associated with the post. Now, all of our votes have to be associated with the
post, because we put in that constraint that check. So that's why we don't see anything with a null
on
the left side, because that's just the way our tables have been set up. So hopefully that
didn't confuse you guys, we're going to change this back to a left join. And what we're going to do now
is
we're going to do, I want to be able to count the total number of votes for each and every
post. So we're once again going to do the group by. So we're going to group by
posts, dot ID. So we'll group them together and then count them. And for columns, I'm just
gonna say, we'll say posts dot ID. And then we want the count. So we'll
start off with the star. And let's see what happens. Okay, so we can see that, you
know,
post with an ID of 10, we know it has two votes. But then everything else has a value of
one.
This is not correct. We know that a whole bunch of posts don't have a value of one. But like
we
covered in the previous lesson, anytime you have a value of null, then it's going to count
that as
one. So I'm going to run this the previous query, remove the group by and I'll show you guys
exactly
what I mean. And now if I run this, oops, there should be a star. All right, look at all of
these
posts that have a value of null, it's going to count them all as one in this case. So
let's
see what happens in this case. And we don't want that. Right? That's why that's what happens
when you do star in the count field. So instead, what we're going to do is we're going to provide
a
specific column to count on so it doesn't count the null values. So what column should we use?
I mean, we can really use any column, I think it makes sense just to do, we have to grab one
of
the columns that are null in this case, so we'll get votes dot post underscore ID. So now,
look
at that, we get the post ID. And then we get how many votes they have altogether or how many
likes.
And instead of just grabbing the ID, I'm going to actually grab everything from the post
table, we can run that. And this is the exact query that we're looking for, we get the post
information,
and we get the number of votes. And we can rename this. Because I don't like calling account,
we can rename it as, you know, votes, or likes doesn't really matter. And there you go,
guys.
And if you guys want to query for an individual post, and get the total number of
votes,
it's actually very simple. It's the same exact query, we're just going to provide a where
condition. And so here, I can just say where post dot ID equals one. Well, actually, I don't have a
post
ID of one, so I'll grab a post ID of 10. And so now I get my one post, and we can see that he
should
have two votes. Alright, and so that's how we do this with raw SQL. Hopefully, this wasn't
too
complicated. Like I said, I'd strongly recommend you spend a little bit of time going over
joins
and practicing it with your own tables, don't just use posts and votes. If you can probably
come up with simpler tables to work on this and understand it a little bit better. But I think that's
going
to wrap up this video. And so go ahead and just save these queries for now so that we can
reference them later. In the last video, we saw how we can gather information from two different
tables
by making use of joins. And so we saw how to do that with raw SQL. And now we're going to see
how we can perform joins using SQL alchemy. And if you go to your post router, we're going to go to
the
get posts path operation. And so we're going to kind of just take this step by step and see
how
we can slowly build out this query, so that we can actually perform a join. And if you take a
look at the current query, what we can do is just ignore all of the filter stuff first. And so
really,
the the meat of this query is just DB dot query, and then you pass the model, and that's going
to get every single post that we have in this table. And then we perform all of these filters to
kind
of drill down. So we're going to start off with the same thing. And I'm going to save this new
query as results. Or, or we could just say posts underscore votes, maybe, and we'll just save
it
as results. And so we're gonna start off with the same exact query, we'll do the DB query
models,
dot post. And we're from dot all, actually remove the dot all. And the reason I don't want to
do the
dot all is I don't actually want to query the table, I just want to actually see the raw SQL
that's generating. And now we can do a print results. I'm going to bring this up a bit.
And
we're going to just send a query to the get all posts. And you can see the raw SQL
that
it generated. So it's going to select and then basically select every column and then rename
it accordingly. And then it's just going to get that from the post table. So it's getting every
single
post. That's all. So everything is pretty straightforward at this point. And if we go
back
to our PG admin, let's actually take a look. Let's take a look at what our SQL query actually
looks
like. And so you can see here, there's a whole bunch of things going on. But the next thing I
want to do is I want to actually start to perform the join. So I want to join on the votes
table.
So let's take a look at how we can do that. And to perform a join with SQL alchemy, I can just
say join. And then we have to specify the table we want to join. So this is going to be the
models
dot vote. And then the second thing is what is going to be our the what is going to be
the
column that we perform a join on. So in this case, you can see that we're performing a join on
post
dot ID and votes dot post underscore ID. And we're going to do the same thing here, I'm going
to say
models dot vote dot post underscore ID equals equals, and then we'll say models dot
post
dot ID, whenever they're equal, we're going to join the table. Now the thing about this join
is, by default, with SQL alchemy, this is going to be a left inner join. And the word inner is going
to
be a little bit new for you. And it's because there's two different types of left joins,
you've got left, inner, and you have left outer, and I don't want to spend too much time kind
of
diving into what's the difference. Just know that if you ignore the keyword, it's going to by
default
be an outer. So what we want is an outer join. However, SQL alchemy by default, uses inner
joins.
So to actually set this as an outer join, we have to pass one more thing, which is going to be
is outer equals true. So this is going to make this a outer left outer join. So we've got
our
join at this point. And what I'm going to do now is save this again. And let's take a look at
our
query or send something. And now we'll take a look at a query, we can see that okay, we're
going to
select everything from our post table, then we're going to perform a left outer join with
votes on
the table votes whenever votes dot post ID equals post dot ID. So we're almost there. The next
thing
that we have to do is we have to group by post dot ID. And then we have to perform the count
on
votes dot post underscore ID. And so if you can take a guess as has to do group by we can just
do
dot group underscore by, and then we specify the specific column. So do models dot post dot
ID.
And then we have to get the count. So we'll go to here. And I'm going to say, well, first of
all,
we have to import a function. So what you're going to do is from SQL alchemy, import funk. And
so
this is going to give us access to functions like count. I'm going to say funk dot
count.
And it will say models dot vote dot post underscore ID. We'll save this. And I'm going to send
this
in. Let's take a look at the query once again. And so we've got select posts, all of the
post
columns. Good, good, good, good. And then we get to count. Right, and it's going to perform
the
count. That's all right. However, we're naming it as count one. I don't like that. I want to
name
this something that I will understand. So maybe something like votes. So we have to figure out
a way to rename this column. And if you want to rename this column, you just say, dot
label,
and then the name that you want to give that column. So we're going to call this
votes.
And so let's test this out one last time. And now we can see that the column is called votes.
So
this is exactly what we want. This is the exact query that we generated in PG admin
manually,
we're just doing it through SQL alchemy. And what we're going to do is, well, let's
actually
perform the query. We'll do dot all don't need the print anymore. And we're going to
actually
return results. And let's see what happens. So now if I send a query, it's giving me a whole
bunch
of errors. And if we scroll to the top of the errors, we can see that these are all pedantic
validations. And so it's saying that, hey, there's no title, there's no content, there's no
ID,
there's no owner ID, there's no owner, all of these fields seem to be missing. And why
exactly
is that? Well, let's take a look at the response model. So the validations happening because
we use this response model of schemas dot post. And so if we go to schemas, and we find our post
object,
or post class, you can see that expects an ID created at an owner ID, an owner, and all of
that
good stuff. And it's saying that none of these have been set. So it looks like there's
something wrong with our query. But just to make this a little bit simpler, what I'm going to do is
I'm
actually going to remove the response model. So I'm going to just comment out that remove
the
response models, we're not actually performing any validation. And we're going to see what
this looks
like now. So when we retrieve the data, I want you to take a look at what this looks
like.
So this right here is one specific post. And so after our changes, we can see that we've
got
a property called post. And then within there, we have ID published and so on. And then we've
got a property of votes. And I'm actually going to copy this as an example, I'm just going
to
create a new file. And I'm just going to say, we'll just call this example, that py that's
fine,
it doesn't really matter. We'll delete this in a bit. So this is what it looks like. And when
we go back to our path operation, I'm going to return posts. And we can actually return the
response
model just to see what that looks like. And so now if I do a query, I need to save. If you
take a
look at the query, this is what it looks like in a working state before we set up the
joins.
And if I go down to my example, where's my example, py file. So this is what pedantic
expects,
expects to get an object with a field of title, content, published ID, and so on. However,
after our join, something odd happens, we have a field called post. And that breaks
everything
because it doesn't expect the ID and the published in the owner to be under this, this
essentially this dictionary right here. And that's why it's throwing all of these errors. So how exactly
do
we fix this? Well, what we're going to do is I'm actually going to go to my schemas. And
I'm
actually going to create a new schema. So we'll call this, you can call this whatever you
want,
maybe post a vote, or I'm just gonna call this post out. And it's gonna be a post base,
it's
going to expand post base. And what we're gonna do is we're just going to try to match this as
close as possible. So this is going to represent a post object, and then we're going to have a
field
called votes, which is going to be an integer. So here, I can just say, we have something
called
post. And don't forget to capitalize it, because our query, for some reason, returns it with
the
capital P took me a little while to figure that out. I was running into a few
issues.
But we can go here, and we can say I want to return, I want this to be of type. And then we'll
reference this post. Right? So all of these fields will be under a field name post.
And then we expect something called votes, which is going to be set to an
integer.
So let's try this out. And we'll go back to our post up p y. And I'm going to call I'm going
to reference post out.
And we're going to return results again. And let's just make sure there's no errors. It looks
like
it's good. And let's see what happens now. All right, we got a little bit of an
issue.
And it looks like there's a title content, title content, a whole bunch of errors. And so the
title
and the content are missing for both. So what did we do wrong here, this should have fixed our
issue.
Well, let's try adding this class config or I think that might actually be what's
causing
this problem. And now let's try this. That didn't look like it fixed it. Alright
guys,
so I was playing around with it. And then I don't know what exactly fixed it. But all I did
was I had changed this to a lowercase p. And this led to, you know, us seeing this specific
issue,
where it says, you know, hey, this post doesn't exist. And then as soon as I recapitalized
it,
all of a sudden, it started to, to work again, it starts to work. Now, I'm not really sure why
it broke in the first place. I didn't change any other code.
Just looking back at my path operation, you can see that I'm now changing my response model to
be schemas dot post out. And then we're performing the same results query and then
returning it. For some reason or another, it's now properly working. I have no idea what
changed.
I have no idea exactly what changed, but it's working now. So hopefully you guys don't
run
into any issues. And hopefully it was just some kind of issue on my machine. But if you do
run
into the same issue, just try moving it to a lowercase tries restarting the application and
changing it back, I have no idea why that would fix it, but seems to have done something. And
so
now our results are perfect, we get the post information, we still fetch the owner, which is
fantastic. And then we have the votes and then just double check to make sure that all of
your
votes are all okay. And so it looks like they all look good. I know that one had two and then
the
rest should all have zero. So everything's looking pretty, pretty good. The next thing that we
need
to do is actually go ahead and add our filters back in. So let's see if I can still filter
on
all of these. And so I'm just going to copy this. And then before I perform the dot
all,
I'm going to paste that in there. Whoops, I forgot to I forgot to copy.
All right, and then let's test this out again. I think they're still working. But let's just
add a
few extra query parameters. So I'll say search equals beaches. All right, and then it looks
like
it should only return post with the word beaches. And then let's try limit equals
two.
Remove this search for now. And that works too. So it looks like we get to keep all of the
same
functionality that we had before. So I'm going to just write this as posts, we're going to
comment
this out, just for reference. And then we're going to return post like we normally do. Send it
one more time. And it looks like everything is still working just fine. Perfect.
So we finished the get multiple posts path operation or get all posts. And I noticed
that
we do have to go ahead and update the get individual post. Because right now, if we go
to
get one post, you can see that we do not actually get the votes vote count for that. So we
definitely
want to see that in here. And we're going to have to perform the same exact join. So I'm
just
going to copy the previous query all the way up to before the filter. So just copy that
part.
Say post equals paste that in there.
And then we can filter copy this filter at the end.
And that should ultimately get us what we want. I'm going to comment this one out. And keep in
mind, we have to return post out. Instead of post now, because we want the new
format with the votes. But now if I hit send, name post is not defined. Oh, okay, misspelled
that.
And looks like we fixed that one. And then it's really up to you to see if you want
to,
you know, kind of worry about creating posts, and updating posts, to make sure that you
know,
when they create an update that they return the number of votes, but it's ultimately up to
you,
I, I think we don't need it in those. This more of just kind of returning back, just the
main
information about the post. And then you know, the queries for updating a specific post, and
then
grabbing the votes gets a little complicated. So we're going to ignore that for the update,
and the and the create post functionality. Now, when it comes to building out the tables or
the
database schema, SQL alchemy has a little bit of a limitation. And that is that SQL alchemy
doesn't
allow us to modify tables, it doesn't allow us to create extra columns, delete
columns,
add foreign key constraints. And that's because when we define these models, what SQL
alchemy
does is it checks to see if the specific table name already exists in our Postgres database.
And if it does, it's not going to touch it. So if we make any changes, it will never push out
those
changes. It'll only create these tables if it doesn't already see a table with that
specific
name. And so we've had to resort to dropping all of our tables, and then restarting our
application.
So that SQL alchemy builds it from scratch. That's something we can obviously do in
production.
That would be an unacceptable thing to do. And just to show you guys, in case you've
forgotten, if we go to my our Postgres database, and we go, we could see that we've got our three
tables.
Now, if I add a phone number class, sorry, a phone number column to our user
class,
and I'll set this column to be just a string, and this is really just for demonstration
purposes, you guys don't have to do this. If I save this and reload this SQL alchemy will not go
into
Postgres and actually create that column. If I refresh this, and then go into users
properties,
you can see that it is not here. And so that's the limitation of SQL alchemy. And so in
this
section, what we're going to do is we're going to take a look at another tool that will allow
us to do a couple things. First of all, it's going to allow us to automatically update the
columns
based off of the models that we define here. So moving forward, when we add this phone number
column, it's going to automatically update our Postgres database. And this tool is called
Olympic,
it is what we refer to as a data database migration tool. However, it's much more powerful
than that.
It's able to allow us to, you know, create incremental changes to our database and
actually
track it kind of like we do with code. So in the in this lesson, and in the next coming
lessons,
we're going to take a look at Olympic, we're going to see everything it has to offer. And I'm
going to hopefully get you guys at least a solid understanding of how to work with Olympics
so
that when you guys work on your projects, you can immediately get Olympic installed running
and then manage your databases using Olympic instead of having to either do it manually, or use any
other
kind of unusual workaround. Now, one of the motivations for a database migration tool
is
came from the fact that developers can track changes to their code and roll back their code
easily with Git. And they wanted to be able to do the same thing for our database models and
our
database schema. And so they went about implementing what is referred to as database migration
tools.
And so a database migration tool just allows us to incrementally track changes to our schema
and roll back those changes to any point in time. And so as we go through our project, you
know,
we kept adding new tables and new features, what a database migration tool is, it's going to
allow
us to track those changes. And so if we ever want to go back to a specific point in time, so
that we can kind of troubleshoot code at that point in time, it's so easy, it's just one simple
command,
and it's going to automatically update our database, so that the schema matches what
it
was at that specific point in time. And so that tool that we're going to use, like I
mentioned, is going to be Olympic. And what's great about Olympic is that it can also automatically
pull
all of our SQL alchemy models and generate the proper tables based off of them.
Alright,
so this is the documentation for Alembic. So it's just alembic.sql.com.org. And if you want to
kind
of follow along, just select the tutorial right here. And this is just going to give you kind
of a quick walkthrough of how to get Alembic set up. But I'm going to show you guys this so you
don't
actually have to follow the tutorial. So we're going to do a first of all, we have to install
the library. So we'll do pip install alembic. And so that will get alembic installed. And what
it
does is it actually gives us access to the alembic command. So if I do alembic dash dash
help,
and actually, before we proceed any further, I want you to go ahead and delete phone
number,
we don't actually need that anymore. And then go to your app application, and actually close
that
out, stop it so that it doesn't automatically restart for any other reason. And then also what
we're going to do is I'm actually going to delete all of our tables so that we can actually take
a
look at how this works. So do delete drop. And if you try to drop this table, it's going to
say we
can't drop it due to the foreign key constraint, that's okay, we could just do drop cascade,
and that's going to delete it. And we're just going to do that for all of them. All right, so now
let's
get back to alembic. I'm gonna make this bigger for you guys. All right. And so we'll do
alembic
dash dash help to see what commands we have. And the first thing that we have to do is we have
to initialize alembic. And what that's going to do is it's going to actually create an alembic
directory
for us. So we'll do alembic in it. And then I'm just going to do dash dash help to see if we
need
to pass in any extra flags. And then we have to give it a directory name. So when we run this
command, it's going to create a directory, I'm going to name the directory alembic. You
could
call it DB database, whatever you want to call it, but I'm just going to call it alembic. So
it's going to create a folder called alembic. And so you can see that we've got this folder
called
alembic. And notice how it gets installed outside of the app folder, which is what we want.
And it
also created this alembic.ini file. So we'll take a look at both of those. And the first thing
that
we want to do is go up into alembic. And there's really one main file, it's the env.py. So
this is
kind of like the the King Kong configuration file. And there's going to be a couple of things
that
we have to add to this file to make sure that things work. Because alembic works with
SQL
alchemy, and the models that you build with SQL alchemy, we need to make sure that it has
access
to our base, our base object right here. So we have to import this base object into the
env.py
file. So I'm going to just go right here. And we're going to say from and what we're going to
do is
how do we actually get to that database file that, that base object, we have to go into
app.
And then we go into, go into database, and then we'll import base. All right. And so this is
kind
of give us access to all of those SQL alchemy models. And then for target underscore
metadata,
instead of saying none, what we want to do is we want to get base. So we're
accessing,
this should be a capital B. Actually, let's just double check that it's capitalized.
Yep,
it's capitalized here. So we'll go to base, and then we want to make sure we grab
metadata.
So that's all we have to do to rig this up to SQL alchemy. The next thing that we have to
do
is if we go to alembic.ini, we have to pass in one value here, which is the SQL alchemy
URL,
which is basically what's the URL to access our Postgres database. And so just to show you
what
this is going to look like, this is going to be fundamentally no different than the URL that
we used within database.py, which is this one right here. And so I'm just going to write this
out.
It's going to copy this fresh part right here, because I can never remember
that.
And so here, gonna copy that. So we say, Postgres ql. And optionally, with the
URL,
you can do a plus, and then whatever driver you're using. So if you don't provide the driver,
it's going to use whatever the default driver is. But we're using, let's see, which
driver,
what is our driver? I do pip freeze, it's going to be this one right here. So this is the
default
driver for Postgres. So you don't actually need to provide it. But if you did, you could just
say plus, p, s, y, c, o, p, g, two. But like I said, it's the default one. So it's optional,
right,
then we have to give it the username. So what's the username, that's going to be using to log
into your Postgres database. For me, it's just Postgres, the password is going to be password
123.
It's going to be running on localhost. And then if you want to, you can provide the port,
which is 5432. And then your database name, which is fast API for me. So this is ultimately
what
the URL is going to look like. And at this point, alembic is set up and ready to run.
However,
you know, just like just like we did before, I don't like the fact that we're hard coding this
data in here. It's gross, we're saving our password. And if we move into our
production
server, it's not going to work properly, because they're not going to use these credentials or
this IP. So instead of doing this, what we're actually going to do is we're going to
override
this value that's stored in this file within the env.py file. And so under this config right
here,
I'm going to set a new option under the config object, I'm gonna say config dot set
underscore
main underscore option. And then here, we can override any options that we have in here.
So
I'm going to override this SQL alchemy URL. So no matter what I put in here, we're actually
going to override it. And here, you just pass in a string. So SQL alchemy dot URL. So that's
the
value we're going to override. And then we're going to pass in our string. So I can just
cut
this out. And it's okay to just leave this blank here. All right. And so we've hard coded
this
string here. So we haven't technically fixed anything. But what we're going to do is just like
we have in our database.py file, you can see that we're using the settings object,
right,
that comes from our config file, which is making use of this pedantic class, where we can grab
it
from our environment variable. And so what we're going to do is we're going to do the same
exact thing we did with main, right, we imported settings. And we're just going to do that
in
our env.py file, so we can access all of our environment variables. So I'm going to say
from
app dot config, import settings. And so now I have access to the settings object, I can
override all
of these. So this is going to be the username. So I can say settings dot username. And
actually,
I don't think that's what it is. It's database underscore username. And then we'll override
the
password, which is settings dot database underscore password. And in reality, instead of just
typing
this all out myself, we actually have this written out in our database.py file. So I can just
copy
this here. And that way, we don't waste too much time. Okay, so we've got our URL set, and
we're
not hard coding any values. And at this point, alembic should be set to actually connect to
our
database and modify any of the tables, generate tables, and be able to really perform any
actions
that we wanted to moving forward. Hey, guys, I made one tiny little mistake. If you take a
look
at where we're importing base, I wrote app dot database. And if we take a look at that, it
looks like we are grabbing our base right here. But this is not the one we want. Instead, what we want
to
do is we want to import it from models. So we want to import base, which is technically
getting imported from that same file. But by doing it from here, it'll allow alembic to read all of
these
models. And if we do it directly from database, it's not going to work. So what I want you
guys to do is go to your env.py file. And just change this from app dot database to app dot
models.
And that should fix any potential issues that you could have. Okay, so just to quickly
recap,
we have cleared out all of our tables. So there's nothing in our Postgres database, we don't
have any tables defined. And we're going to do this all through alembic.
But what I want to do is I want to kind of go through this, like we would have if we had
already known about alembic from the start of our project. And so we're going to take this step
by
step, right? Because when we first started creating tables in our Postgres database, we kind
of went along with what we were working on in our app. So the first thing we did was,
we created a post table. And that's because we didn't learn about how to actually create
users,
we didn't learn about hashing passwords, we haven't gotten to the user section. So we
originally just had a post table with a couple of fields. And then after working through our code, once we
finished
all the CRUD operations for posts, we then learn how to register users. And at that point, we
had to create a table for users. And then after that, the next logical step was a how do we set up
the
relationship between posts and users, then we had to modify that and set up a foreign key. And
then
finally, after that, the last thing that we had to do was we created a voting table, or a
votes table. And then we set up all of the necessary foreign keys for that. So we're gonna, so
we're
going to do this step by step, just kind of like how we did it with our our project. But
we're
going to do this with the database so that you can see how we would have done it if we had
already known how to work with alembic. Alright, so first things first, do alembic,
alembic,
dash, dash, help, just to see what commands we have. And so there's a few different
commands.
But the first one that I want to focus on is revision. So when we want to make a change
to
our database, we create a revision. So the revision is what really tracks all of the changes
that we
make on a step by step basis, they're going to do alembic, revision, dash, dash, help. And
we'll
see the different options that we have in our disposal. The first one is that I really the
only
one that I care about is the dash m flag. So it's kind of like when you do a git commit, you
can add a dash m a message, so that you can kind of have a human readable name associated with each
revision.
So I'm going to say git, sorry, not git, alembic, revision, dash m, and I'm going to say this
is
going to be a create post table. So that this revision is going to be responsible for
creating
a post table. We'll run that. And I want you to notice something, right, you can see that
it
created this file under the version. So the versions folder under the alembic folder is going
to contain all of our changes. And if we take a look at the revision, there's a couple of
things
to know. So you'll see that we import op from the alembic library, and then we import essay
from the
SQL alchemy library. And then you can see the revision ID that it gives it. And then there's
going to be two different functions that are empty at the moment. So these functions are
pretty
important. What they are is the upgrade function. When we run the upgrade, what it does is it
runs
the commands for making the changes that you want to do. So in this case, we want to create a
post table. So what we're going to do is we're going to put all of the logic for creating a post
table
within this function. And then if we ever want to roll back, right, if maybe I create the
table,
and I realized I messed it up, and I don't want it anymore, then we have to put in all of the
logic in the downgrade function to handle removing the table. So the upgrade just handles the
changes.
And the downgrade handles rolling it back, but it's all manual, you have to define how to do
all of that. So let's go ahead and set up the upgrade function. And let's configure all of the
necessary
columns for our post table. Alright, and if you ever want to take a look at the documentation
for
this, what you want to do is select documentation here. And then you want to go down down down
all
the way down to API details. And you want to go to DDL internals. So this is going to show you
how
to actually perform the various operations. So if you want to create a column, if you want to
add a
column, this is the command that you're going to run if you want to alter a column. If you
want to modify a table, drop a column, create a table, all of these are all listed here. So this is the
sheet
that you're going to reference on a fairly frequent basis when you're working with alembic,
because I can never remember each of the things. But we're going to go ahead and create our post
table.
And so we access the op object from alembic. I'm gonna say op. And then if you look at
the
documentation, the way to create a table is you do create underscore table. And there's going
to be
a couple of properties we have to pass in. So what's the name of the table in Postgres? Well,
we're going to call it posts. And then we have to define our columns. And I'm not going to do
all
of our post column, because it's going to take a little bit of a while. And we might do it
later on, but we're just going to define two columns. So the first one is going to be, well, first of
all,
we do essay column. So we're grabbing the SQL alchemy object. And so this column is going
to
represent the IDs, the name of it's going to be ID. It's going to be type of integer. And I
may
have accidentally imported nope, that we're good for integer. And then we can set nullable
equals
false, which means it's required. And then we also want this to be the primary key. So you
just do
primary key equals true. All right, fairly straightforward, right, guys, so we can
then
go in and create a second column. And so this is just going to follow the same exact pattern
essay
dot column. And then this one will be the title, it's going to be a type of essay dot
string.
And this is going to be nullable equals false. And I'm just going to stop it right here. So
we're
going to just create two columns, I don't feel like creating all of the columns that we had
for posts.
And so the upgrade function is done. However, we also have to provide the logic
for
undoing these changes. So if we create a table, what's the how do we undo that? Well, we
delete the table. So we do op dot drop underscore table, and we just give it the name of the table. So
it's
going to be posts. Alright, and so now we should be able to make these changes. And then we
can roll back because we put the logic in for downgrading. So how exactly do we make
these
changes? Well, let's go to alembic dash dash help. And let's see what commands we
have.
So we've got a couple branches, that's probably not it current, that's going to show us what's
the current revision, like what version are we on? And if you actually want to see
that,
we'll see alembic current. And it looks like it's giving me an error. Hold
on,
let's go back to my env dot p y file. And I made a very silly mistake. This should be an f
string.
So it wasn't actually doing anything. That's why it was kind of highlighted weird. So let's
try this now. Again, we're going to do alembic current. And it's not going to actually really give us
any
useful information at the moment, because we haven't run any migrations, right? We're kind of
starting from a clean slate. So let's go back. And let's just do a dash dash help and see
what
else we've got. So we've got a downgrade. Well, we don't want to downgrade edit, not
really.
Heads, we'll come back to that history. That's not going to be useful, because we haven't
done
anything. But if we go down, we want to get to upgrade. So this is what's important. So if we
do
alembic upgrade, we can just do dash dash help. And we'll see what options we
have.
And so we have to provide a revision number to tell alembic like, hey, what version of
our
database we want to go to. And so if you actually take a look at this specific revision, it
provides
a revision number. So if we actually want to get up to this point where it runs this upgrade,
we provide these revision number. So I can say alembic revision. And then paste that.
So let's run this and see what happens. Sorry, not, I didn't mean to type revision, I meant
alembic upgrade. And then that not alembic
revision. So that's going to upgrade it. And so you can see it did a couple of things to
running upgrade. And it said it created post table. So let's go to Postgres. And let's just refresh
this.
And let's see if it actually did something. And it looks like it created two tables. So that's
interesting. So we are we see our post table. And let's just quickly inspect this. We'll go
to
columns, you can see that it created these columns. And this one got set to the primary key
and both are not now. So it looks like it did everything it was supposed to do. But what is this
alembic
versions table? Well, this is, this is what alembic uses to kind of keep track of all the
revisions.
And so if we just go to view edit data, all rows, you can see that it just has one column at
the
moment, which has the revision number, you can see it's D one eight, and that's going to match
up with
this file D one eight. So that's all that's doing. So it will create a table for you
automatically, so you can keep track of all of the revisions. But make sure you don't delete that.
Okay,
so that's cool. We just got our first table created through alembic. And I think at this
point, you probably still don't understand the brilliance of a database migration tool. And that's
okay,
because we haven't really done much with it yet. So let's say that, you know, as we're coding
out our application, we realize we want to add a new column to our database, sorry, well, to
our
specifically to our post table. Well, we can create a brand new revision to add a new column.
So let's say, if you take a look at our models, you know, we have a couple other fields that are missing.
So
let's say we want to add the content column now. I can type in alembic revision. So this is
going
to create a brand new revision, we'll give it a name and I'll say add content column to post
table.
All right, and it's going to create a brand new revision for us. And once again, we have to
put in the logic for upgrading and downgrading. And you can see that
when we now that we have more than one revision, we can see that there's also a down revision.
So if you want to go down a step, it's going to go to the revision of the previous one. So
let's
put in the logic for creating or adding any brand new column for content. And the way we add a
new
column is we do op dot add underscore column. And then we specify the table we want to modify.
So
it's going to be the post table. And then we do SA dot column to define this
column.
So what's the column name, it's going to be content. And there's gonna be a type of
string.
And we're going to make sure that it's nullable is set to false.
All right. And remember, every time you set up the upgrade function, you have to set up the
downgrade function. So if we add a column, what's how do we undo that? Well, we drop the
column.
So it's just op dot drop underscore column. And then from the post, we have to provide the
table
that we want to drop the column from, which is the post table. And then we need to provide the
actual column we want to drop, which is going to be content. So we'll do content. Okay, and so
let's
test this out. First of all, we'll run a few new commands. So alembic. All right, and let's
see
what the current revision is. So if I do alembic current, right, we could see that we are
currently
on 2d 18. That's the revision. So this is the the revision where we created the post table.
And if
I type in the alembic heads, we can see that the latest revision is actually 298, 298 a five,
which
is that new one we just created. So if we ever want to get to the latest revision, that's
what's referred to as the head. So I can say, if we want to, you know, upgrade to that revision, I can
say
alembic upgrade like we did before. And I can grab the revision number like this. Or because
it also
happens to be the head, I can just say alembic upgrade head and go do the same thing. So let's
try
this. Alright, it said it added a content column, we'll go and check this in Postgres. And I'm
just
going to refresh this table. And we'll go into properties. And we can see that we added
the
content. Now, let's say that after working on this for a bit, we realize we want to undo
these
changes for whatever reason, maybe we no longer want the content column. So how do we
actually
roll back? Well, the since upgrading makes the changes downgrading is going to do the
opposite.
So we can do alembic downgrade. And then if you want to just roll back this revision, we can
see
what the down revision is. So we can just say this, and just paste that in there. But we can
also do
a few other things, we can also say minus one. So when I say minus one, that means I'm going
to go
back to one revision earlier. And we can even go as far back as we want. So if I say I want to
go back to minus two, it's going to go back to two revisions earlier. But we just have one. So
I'm
just going to say minus one, or I can just paste in the revision number. And so if I run the
downgrade,
you can see that it ran this downgrade. And if I go and refresh this, and then go to
properties,
columns, we can see that we no longer have a content column. And that's how easy it is to
downgrade
or roll back your database using a database migration tool like alembic.
Okay, so we got our post table, we're not going to worry too much about that. The next part
of
our application was, hey, let's implement a user functionality, let's make it so that users
can sign up and log in. So now we need to create a users table. How do we do that? Well, let's
set
up a new revision, we'll do alembic, revision, dash m. And I'll say add user table. All
right,
so we've got our brand new revision. And we have to set up the upgrade and the
downgrade.
Now for this, I don't want to just type this out, you know, one by one, it would be kind of
slow and boring for you guys. So I've actually I'm just going to copy and paste this from my
notes.
And if you want to follow along, just, you know, pause the video, and just write this out.
But
we'll just quickly, you know, walk through exactly what's happening. So once again, we're
using the create table method, we're going to create a table called users. And then we're going to set up
all
of our columns. So we've got a column called ID, which is an integer set to nullable equals
false,
you notice how I haven't set this to be the primary key here. That's because there's two
different ways to set the primary key. You know, if we go back to the previous one, we set
this
to be primary key equals true. But on this one, if you actually go all the way down to the
bottom,
you can see that we can add in a constraint as a primary key constraint. And then we pass the
name of the column, that's going to be the primary key. So they both do the same thing. It's just
whatever
method you prefer. Now we've got the email column, which is going to be set to nullable equals
false,
same thing with password, then we've got the created at. And so here, it's going to be a
type
timestamp. And you'll see that this is all coming from SQL alchemy, essentially. Because if we
go to our models, and we go to our user table, it's the same thing type timestamp, timezone equals
true,
nullable equals false, server default equals text. Well, that's created at but yeah. And we're
just
doing the same thing. So server default, and then we grab SA dot text, now, nullable equals
false.
And then you can see that we added a unique constraint under the email field to ensure that we
can have duplicate emails. And then to downgrade, we just do op dot drop underscore
table
users. All right. And so once again, if you want to see where we currently are, because
remember,
we had originally upgraded added the content column, but then we downgraded. So where
exactly
are we from a revision perspective? Well, let's do alembic current. You can see that we're on
2d1.8.
So we're back on that fresh revision. And we can also do an alembic, I believe it's
history.
And you can see the history of our revision. So we created post, add content column to
posts,
and then add user table. And this is the head. So if you want to go all the way to the front
to the latest, you would just do alembic upgrade head. And I think there's one more I want to show
you.
Yeah, that's okay. So we're going to do an alembic. And let's say that we just want to go and
go up
one revision. So adding one revision is going to add that column back, we can do alembic
upgrade.
And we can either put in the name of this revision. Or we can upgrade one
level.
If we want to go all the way to the latest, which is going to be adding the user table, we can
say head, we can also go up to revisions. So if I do plus two, that's going to go up to
add
content column one, that's the first revision, and then one more, which is going to go up to
the latest in this case. So there's a lot of different ways to get there. I'm just going to go to
the
latest. So this is going to add the column back in the post table and then add the users
table.
And you can see that it went ahead and added the users table. So if I refresh this, we can now
see that we have a users table. And hopefully all of the properties are there. You
can see all of these have been set constraints, primary key is set. And anything else,
everything
else looks good. I guess one thing just to check is the created at so if we check the
constraints
here, you can see that we're going to grab the current time. Perfect. Alright, so after we
had
added the user functionality, the next thing we had to do was we had to implement the
relationships
between users and posts. So we had to set up that foreign key that linked the user table to
the post table or the post table to the user table. So once again, we're going to do
an
Olympic revision dash m. I'm just gonna say add foreign key to post table. All
right,
we got our new revision. And what we're going to do is we're gonna first of all, we have to
add a column to our post table called owner ID. And that's going to be the one that has the
foreign
key constraint. So we have to do op dot add column. And we have to specify the table we want
to add
the column to which is going to be the post table. And this is going to be essay dot
column.
We're going to call this owner underscore ID. And it's going to be a type of essay
integer.
And nullable is going to be set to false. All right, so we've created the column. Now we
have
to set the link between the two tables, which is going to be the foreign key constraint. And
so to do that is a little bit confusing at first for me, because the documentation is a little bit hard
to
read sometimes. So you do create foreign key. Then you have to give it, you know, some
arbitrary
foreign key name, I'm gonna call this posts underscore users underscore FK. This is the
foreign key between the post and the user's table. And then we have to specify what is the
source
table of the foreign key. So this is going to be the foreign key for the post table. So we're
going to say source underscore table equals posts. Then we have to reference the the remote
table,
which they call a referent underscore table. And so this is going to be the users
table.
And then we have to specify what is the local column that we're going to be using. So the
column in the post table. And that's going to be owner underscore ID, which is the
table,
the column that we just created. And then finally, we have to specify the remote
column
in the users table. And this is going to be set. And we're also going to set the on
delete
to be cascade. And yeah, by the way, the remote column is going to be the ID field of the
user
table. And we want to make sure we have the on delete as well. And that pretty much
Oh,
what happened there? It's a weird way to format things. All right, well, it wants to format
it
that way. So I'm just gonna leave it like that. Let's set up the downgrade now. So we have to
undo those changes. We'll do op dot drop underscore constraint. And then we just mentioned the name
of
the constraint, which is post users fk. We have to specify the table that we're removing it
from. So
it's on the post table. So we do table name equals posts. And then we have to drop the column.
So we
do op dot drop or column. And where does this column sit? It's in the post table. And it's
called
owner underscore ID. Alright, and so if I do an alembic current, we're on f5 to an alembic
heads,
let's see what the latest one is, it should be 296, which is this one. So let's do an alembic
upgrade, head, go to the latest. And I have a typo. So this should be integer. All right, looks
like
it ran with no issues. Let's double check that the foreign key is there. Now, if we go to
posts, properties, constraints, foreign key, you can see that we have our post user fk. And we can see
that
it points from owner ID in the post table to ID in the users table. And if you want to, you
can
go under actions and see that cascade has been set to on delete. Now in our post table at the
moment,
you know, we didn't implement all of the columns that we actually have in our application. So
I figured out why not just go in and add it might as well. So I'm going to create a new
revision.
All right, and then we go to that revision. There's really only two more columns that we have
to add.
And this is I'm once again going to just copy and paste from my notes, just because I don't
want to waste your guys time too much. But you can see here, we're going to do
an add column. And we're going to add it from our add it to our post table. And then here,
we're going to specify the parameters of the column. So this is going to be called
publish,
it's going to be of type Boolean. And nullable equals false. And that's not correct, it
should
actually be nullable equals true. Right, it should be actually no, it was false. And then
server
default is set to be true. So if we don't provide it, it's going to assume it's true. And then
the
next one is the column for created at. So once again, at the post table, we're going to
create
a column called created at, it's going to be a type timestamp, nullable false, and then the
default means we're going to grab the current time. And to downgrade, we're going to delete
those
columns, right? And let's do a, a limbic.
Upgrade. And we'll just go instead of doing head, we can just do plus one, just to show you
guys how it's going to do the same thing in this case. And so it looks like it ran that. And if we
go
back to Postgres, refresh this, and then go to properties, we should see, we've now
got
our published and our created at columns. All right. And once again, let's say that maybe
we
want to roll back, right? Maybe we want to roll back all the way to back when we just
created
the post table. You know, we can once again, do a limbic downgrade, and just provide the
revision
number of the specific revision you want to go to. So we can go down to add user table, we can
go down to create post table, we can go down to any section we want. And it's really
up
to you. And so let's say this time, let's just go all the way down to, let's go down to add
user
table. So whatever revision this is, we can do that. Alright, and so it deleted a whole bunch
of
stuff. It looks like it deleted the last few columns that we just added, as well as the
foreign key constraint. And so now, if we refresh these tables, you can see that posts, it doesn't
have
the extra columns we added. And the constraints foreign key no longer exist. But
fortunately,
we can upgrade to a newer version to one of the more recent versions. So I can do a limbic
upgrade
plus one. So if I do this, it's going to go up one newer revision. So right now we're at
the
add user table. And if you forget wherever you are, you can just do a limbic
current.
Alright, so we're on three to F five, two. And so if I do an upgrade,
right, we're gonna go up to I think it's this one. Because you can see that in this
one,
this revision is 296. But the previous revision is 252, which is the one we're currently
on.
So I can do a limbic upgrade plus one. Alright, and so now we've added the foreign key. If I
do a
limbic current, we can see that we're on 296. And then if I just do a limbic upgrade, a limbic
heads,
it's going to show us whichever one's latest one, which is five b seven. So we can just
upgrade
directly to that by doing head. The last thing that we have to do now
is generate the votes table, but I'm not going to manually create it myself this time.
Instead,
I'm going to make use of the auto generate feature. So this is what I was talking about
earlier, how we can use a limbic to be intelligent enough to figure out what exactly is
missing
from our Postgres database and figure out what needs to be created. And the way it actually
does that is it takes a look at all at all of our models. So it's going to import all of our
SQL
alchemy models. And based off of the columns that we have set here, it's going to figure out
what our Postgres databases is supposed to look like. And regardless of what the our database
is,
in its current state, we don't need to actually delete everything and build it from scratch.
Instead, a limbic is going to be intelligent enough to figure out what columns are
missing,
what tables are missing, what extra columns do we currently have that we may need to remove,
and it's going to figure out what's different between SQL alchemy models in our
Postgres
database, and it's going to make the necessary changes for us. And so the way to do
that,
and first of all, I guess, just to kind of reiterate, the reason we can do that is because we
imported the models, and then we passed it into this target metadata. So let's run a
limbic.
And we'll do revision. But if we do dash dash help, you'll see that we're going to use one of
the
flags that we have never used before, which is the dash dash auto generate. So we'll do dash
dash
auto generate. And then we will send a message once again, we'll say add votes about auto
vote,
maybe, because it's auto generated. And I know it's going to add the votes table, because that
should be the only difference that we have. So we'll run that. And so if I go to auto
vote, let's take a look at the changes it's going to make. And so you can see here, the
changes
where it looks like it's going to create the votes table, like I predicted, it's creating the
two columns, and then it's going to add the primary key constraint as well as the two foreign
keys
as well. And it's going to even add in all of the on delete properties and things like that.
All right, and so all we got to do now is do an alembic upgrade head. And now if I refresh
this,
and go to votes, and properties, right, we should have the two columns, and then our
constraints
and foreign keys look pretty good, everything looks set. And so that's how easy it was. So no
matter
what state your databases at any point in time, you can all you can always do the auto
generate
functionality to get it back to a state that your code expects it to be. And the best part
about
this is moving forward. Let's say I want to go back to my models and I want to change
something.
So I've got my user model this time, I'm going to add that phone number column. And this is
going to be string. Alright, so any changes we make, even on a table that
already exists, what we can do now is I can do a revision auto generate. Here, I can just call
it
add phone number. Alright, and then if we go take a look at that, we can see that a look, we
added
our phone number column. And so the only thing else we have to do is do an alembic upgrade
head,
and it's going to push out those changes. So let's refresh this, and let's just double
check.
We go to users now properties, we can see phone number column is now set. And I think
that's
pretty much going to wrap up everything I wanted to cover from alembic. So moving forward, now
that
we have alembic, we no longer actually need this command in our main.py file. This is the
command
that told SQL alchemy to run the create statements so that it generated all of the tables when
it
first started up. But since we have alembic now, you no longer need the this command.
However,
if you want to keep it in, it's not really going to break anything. It just depends on how you
want things to work. Because if it does create the tables for you, then your first
alembic migration isn't going to have to do anything because everything is already
there.
But I'm going to leave this commented out moving forward, because I don't really need it. And
it's really up to you guys for what you guys want to do. For the most part throughout
this
course, we've been testing our API by sending a request from Postman. And Postman is a great
tool, but there's one thing to note. And that is that when you send a request from
Postman,
you're actually sending a request from your own computer. And it's important to understand
this because your API can get requests from different types of devices. You can get a request from
a
computer or a server, whether that's through Postman, or even through curl. If you've ever
used curl, it's essentially doing the same thing that Postman is doing. It just doesn't have a
nice
little GUI can also get requests from mobile devices. But more importantly, you can get
requests from a web browser. So when a web browser sends a request, you know, using
JavaScript,
fetch API, there's going to be a slightly different behavior that we have to account for,
which we can't take into consideration when sending requests with Postman because
Postman
isn't a web browser. So what I'm going to do is I'm going to show you exactly what happens
when we send a request from a web browser instead of our own computer using Postman. So what I
want
you guys to do is open up your web browser and specifically go to google.com. It's important
that you go to google.com, or technically, this is going to work on any website, but I need
you
to actually go to a specific website like google.com. And then I want you to right click on
the web
browser and just hit inspect. So this is going to open up the Chrome developer tools. And if
you are too familiar with it, don't worry, it's just a it's a built in tool to that we have in
Chrome
or any web browser that allows you to make it makes it easier as a web developer to kind
of
troubleshoot things. But what we can do is if you go to console, we can actually send an API
request
to our API from google.com or from our web browser, which is on the google.com domain at the
moment.
And so I want you to just type this in, we're going to say fetch. So this is how you perform
an API request from the web browser level. And we're going to send a request to our past
API.
So that's going to be hosted on HTTP colon slash slash local host, colon
8000.
And then just type in then. So don't worry too much about this, but it technically returns
a
promise. So we have to resolve the promise we just say res res dot JSON. And then do another
then
and just say console dot log. And I forgot one bracket right here. And so all this is doing
is
it's sending a request to the root URL of our API. And then it's going to print out the
contents of whatever we get back from the server. So when I run this, I want you to notice something we get
an
error. It says access to fetch local host, colon 8000 from origin, and then it has google.com
here
has been blocked by course policy. So what is happening here? Why is it that I can't send a
request
from the web browser? But if I go to postman, and I just create a quick new
request,
we can just go straight to URL. It resolves just fine. Well, it has to do with this course
policy.
So in this lesson, we're going to take a look at what exactly this course policy is, and what
we can do to ultimately resolve this issue. So what exactly is course course is short for cross
origin
resource sharing. And so course allows you to make requests from a web browser on one
domain
to a server on a different domain. And by default, you know, when you configure an API,
whether it's
in fast API, or any other framework or language, you will only be allowed to send requests
from my
web browser running on the same exact domain as your server. And so what exactly do I mean by
that?
Well, if our API is hosted on google.com, and our and our website is hosted on
eBay.com,
eBay.com, by default, cannot send a request to an API running on a different domain like
google.com,
it's going to get blocked by cores. However, if our web, if our website was also running on
google.com,
and our API was also running on google.com, then they'd be able to talk just fine by
default.
And just to prove that to you guys, if we go to our web browser and go to localhost colon
8000,
you know, which is the the URL that our applications currently running on, you can see that I
get a
response back. So our web browser was able to make and by the way, when I when I did this,
right, it did the same thing as you know, going into Chrome Developer Tools, and actually sending a
request.
And just in case you don't believe me, I'm just going to actually just show you guys even on
here. If I paste it, you can see that it was able to resolve and we can see the Hello world. So
the
reason why it works on this website is because this is the same exact URL or domain name
that
our API is running on, right, our API is running on localhost colon 8000. So by default,
they're
able to talk to one another only when the web browser domain is on the same exact domain as
the
API is domain. So to fix this, you know, if you did want to allow people from other domains to
talk
to your API, what you can do is take a look at the fast API documentation and just search for
cores cross origin resource sharing. And you'll see that setting this up is really simple. So
the
first thing that we have to do is from fast API middleware cores, we're going to import
cores
middleware. All right, and then just copy this section right here. And I'll explain line by
line
exactly what each one's doing. And so under the app right here, we'll just paste this in.
And
it's gonna throw a little error here, but we'll come back, you can actually delete that and
change that to a an array for now. But what we want to do is first of all, we have to pass in the
middleware
and middleware is a term that's used in most web frameworks, because it's basically a function
that
runs before every request. So if someone sends a request to our app, before it actually goes
through
to our app, and before it actually goes through all of these routers, it'll actually go
through the middleware and then our middleware can perform some sort of operation. But what we want to do
is
first of all, we have to specify the origins that we want to allow. So what domains should be
able to talk to our API? Because right now, it's only it's no domain, only if it's running on the
same
exact domain, are you able to talk to our API. So the way we can do is we can put in a will
create
a list called our origins. And I'll say origins equals and it's just going to be a list. And
here
we're going to perform provide a list of all of the domains that can talk to our API. And
before
we fill that in, I just want to quickly talk about the other three as well. So our course
policy can
be pretty granular. So not only can we allow specific domains, we can also allow only
specific
HTTP methods. So if we were building like a public API, where people can just retrieve
data,
we may not want them to allow, we may not want to allow them to send post
requests,
and put requests and delete requests. So we could potentially just allow get requests. And
then we can even allow specific headers as well. But for now, we're just going to
allow
all headers and all methods. And we're going to just kind of drill down based off of what
origins can talk to us. And so, you know, if we want Google to be able to talk to us,
I can just say HTTPS colon slash slash www dot google.com.
And so now this course policy is set to allow people from google.com to talk to
us.
And so if I go back to google.com, I just hit the up arrow so I can run this command
again,
you'll see that I do successfully get a response back and there's no core there. However, if I
go to a different website, so you know, some website on a different domain,
like youtube.com. And then we go and then we send the same exact request from the
console.
We once again get the course error. And that's because in our in our list of origins, we're
not providing www dot com dot youtube.com. So if you wanted to also allow youtube.com,
then we'd have to do HTTPS colon slash slash www dot youtube.com. For now, I hit the up
arrow.
We can see that it properly resolves. So you just have to provide the list
of
URLs that can talk to your API. And if you want to set up a public API so that everyone can
access
it, then your origins would just be a wild card. So this means every single
domain,
or every single origin. But if you if your API is being configured for a specific web
app,
then you definitely want to make sure that you provide a strict list of origins. So
just
whatever domain your, your web app is running on so that no one else can, you
know,
accidentally reach our application for some reason or another. It's just security best
practices to
really narrow down the scope of the origins that can actually access your API. For
now,
I'm just going to leave this as set to everyone so that you know, when we actually go to
deploy this, they'll be a little bit easier from a testing purpose. But feel free to update this
accordingly.
In this lesson, we're going to focus on setting up Git for our projects so that we can start
tracking our changes, as well as set up a remote repository to store all of our code,
this is going to make the whole deployment process a whole lot easier. But before we set up
Git, there's a couple of things that we need to do. Because when you check your files into
Git,
by default, it's going to check in all of your files. And there's going to be some files that
you don't want to be uploaded to a remote repository, or even into your Git in
general.
And so we have to create a file called a git ignore file. And so if you go to the main
root,
root folder of your project, I want you to create a file called dot git ignore. And it's
important that you put the dot in front of it. So it's dot git ignore. And so this file is going to tell
git,
what are all of the files and folders we don't want upload to get. And so some of the
common
things that you would not want to be tracked within Git is going to be your dot ENV
file.
So this is going to be all of your environment variables. Obviously, you don't want to check
those in or then, you know, if this is part of a public repository, then anyone can see
those.
But also on top of that, you'll see all of these random py cache, these underscore underscore
py cache folders. So I'll go ahead and add that in there as well, because we don't actually
need
to check that into Git. And then finally, we have our folder, which contains our virtual
environment,
we don't want to check that in as well, just because we have no idea if someone else
who
wants to clone our repository is going to be using a virtual environment. And if the if they
are,
then they can create one on their own, there's no need for us to actually upload our virtual
environment, it just wouldn't make sense. And on top of that, our virtual environment contains
all
of the third party libraries that we've installed. So all of the different packages we
installed
through pip, including fast API itself. So all of the code of fast API is within that virtual
environment folder. And so there's no need to actually upload all of that, because that's a
lot
of a lot of files that actually take up a decent amount of space, and it just wouldn't be
efficient to actually upload those to Git. However, this does create one bit of an issue. Because if
we
clone the repository at this point, right, we have no idea what packages we need to
install,
because our virtual environment didn't get uploaded with it. So we need a way for other
users,
other people on our team to know what packages and dependencies we need our
application,
so that our application actually works. And so the way that we actually do this is well, first
of all, before we even proceed, go ahead and copy this into your git ignore file. And
make
sure you have this before you do anything else. Because once certain files get checked in,
it's very hard to get them removed. But after this, what we want to do is we want to
create
one more file, and open up your terminal. And what we're gonna do is type in pip
freeze.
So pip freeze is going to dump out all of the different packages and libraries that we have
installed, as well as the specific versions. And so this is the information that we want to
upload
to get so that if anyone else clones our repository, they'll know the exact versions of every
package
they need to install. So we're going to do is we're just going to take this output, and we're
going to pipe it to a file. And the file name is going to be called requirements.txt. So this
is
standard convention. With any Python application, you want to create a requirements.txt
file,
that's going to contain the version of all of the different packages that you have installed.
And so if we check this into Git, now, then anyone else can just take a look at a
requirements.txt
file, and then actually install dependencies based off of what's in this file. And so for
anyone else that clones our repository, all they have to do, instead of going one by one installing
these,
they can just type in pip install dash R requirements dot txt. And that's going to
install
everything that's in this file. So this does simplify the process. And it prevents us from
having to upload all of those third party packages into Git. But we've got everything set up at
this
point, we've got the Git ignore file, and we've got the requirements dot txt. So what we'd
need to do now is actually set up and install Git on our local machine. If you don't already have
Git
installed, then in this lesson, we're going to focus on getting installed on your local
machine. So just search for Git. And then you can either go to the main page, or you can go straight
to
the downloads page. So I'm just going to select the downloads page. And then here, you're
going to see the different operating systems we can install Git on. And so just go ahead and
select
your respective operating system. I'm on a Windows machine. But regardless of your operating
system, it's going to be a fairly similar procedure. So I'm going to select this, it's going to
download
that. Alright, and then when the wizard opens, go ahead and select Next. Next here, you can
leave
everything default here as well. And here and here. Now, this is the one thing that we do have
to
change. So this is kind of a little bit of a nuisance at this point. But they used to
call
like the main branch, the master branch, but I guess people are offended by that. So now the
new term for the main branches main. So go ahead and override the default branch and just
change
that to main. Because when you work on GitHub, they've already changed the default branch to
be
main. So we're gonna want to do the same thing. So hit Next. And then you can leave
everything
as default moving forward, there's a lot of windows to go through. And then it's going
to
proceed to install Git. All right, close that out. And then what you want to do is open up a
new
terminal and just type in Git dash dash version. And if it spits that out, that means you've
got
Git successfully installed. In this video, we're going to set up a remote repository so we
can
store all of our code on GitHub. And so if you don't already have an account on GitHub, go to
the main GitHub page, just go to GitHub.com and just click Sign up, and then follow those
instructions.
And then once that's done, just log in and you should see something similar to this. Although
you may not see an activity window if you just created a brand new account, because you
won't
have any repositories. But just look for a new button, a button to create a new
repository,
you can also go up here, I believe and select new repository. So let's like new. And then we
have to
give our repository name, you can give it any name you want. I'm just going to give it a dummy
name. So this is going to be example fast API, but name it what after whatever your project
is.
Technically, I've already set up and get from my project, just so I can track all the changes
I've made throughout the course. But I want to make sure that I can walk you through this
if
you've never done it before. You can give it an optional description. And then you can
choose
whether or not a repository is public or private. If it's public, anybody can see it. If it's
private,
then only you can see it, we're going to keep it as public. Don't worry, if you decide to
change it in the future, you can change it to private. However, it does make a few things a little
bit
easier when it's public. So I'm going to leave this as public. And then we're going to leave
all of these unchecked, they don't really matter. And we'll select Create repository. And then
you'll
see that it's going to give us a couple of steps to follow. So if we haven't already set up
Git for our project, these are the instructions we're going to follow. And if we've already set up
Git
in our project folder, then we can follow these steps. However, you know, we haven't set up
Git
yet. So we're going to, for the most part, follow along here. But we're going to modify it
just a little bit, because we don't need to run some of these commands. To go ahead and open up
your
project in VS code, like you normally do, then open up your terminal. And we're going to
follow
these steps. But we're going to ignore the first one, we don't need to read me for now. But we
are going to do a Git in it, which is going to initialize Git for our project. And so make
sure
you're at the root directory of your project. So whatever this is called right here at the
top, make sure that's where you're at. And just do a Git in it, that's going to initialize Git.
And
you may not see anything different about your folders. But I think it's because VS code
actually
hides the git folder. But when you actually did a git in it, if you open up this, your
project
directory, and you click on view here, and then make sure you select hidden items, you'll see
that it created a dot git folder. So this is where it stores all of the git information. Then
we're
going to skip this git add read me instead, what we're going to do is we're going to run git
add
dash dash all. This is going to add all of the files that we have in our directory into git.
All right, and you'll get a couple of warnings, potentially, don't worry about that.
Those won't create any issues. Then we have to commit our changes. So anytime you add a file,
you then have to commit it. So we'll say git commit dash m. So the dash m means we're going to
provide
a message. So you usually give a message for each commit to kind of describe what changes
you're making. So we're just going to call this initial commit. All right. And you'll see that we
probably
get an error if this is the first time you've ever worked with Git. And that's okay. It's just
telling us, hey, we need to set up our user for our account on this machine. So we're just
going
to run this command right here. And then we're just going to provide the email that we use to
register for GitHub. But also get config dash dash global user dot email. All right, we set that
and
then it's telling us to also set our name. So we'll do that as well. Then we're going to
run
that same command, the git commit. So if you just hit the up arrow a couple of times, you'll
get to that. Alright, so we've committed those changes, then what we need to do is we need to set
what
our branches. So here, this is just going to set our main branch to be called main. And so we
can
copy that, paste it into here. Then we have to set up a remote branch. So this is what's going
to
allow us to store all of our code on GitHub. So we're just saying, hey, listen, this is going
to
be the remote Git repository, and we just provide a URL to it. And so that if you take a look
at the URL, it's going to match up with this. And in this case, we're naming it origin, you can
technically
call it anything you want. We'll add that. And then finally, we need to do a git push dash
u
origin main. And so this means we're actually going to push all of our code up to
GitHub.
And you'll see a pop up asking you to sign in. And there's a couple of things we can do, we
can get a personal access token, or we can sign in with our browser.
I'm just going to sign in with our browser. And we just like authorize right here. And says
it's succeeded. So we should be able to go back.
And we could see that a couple things change. So it looks like, well, that's our
authentication. And then you can see that we're pushing all of this stuff up to GitHub. And so at this
point, you're
pretty much done all of your code is now stored in GitHub. And we can just verify that by
going back to this page and just select the name of the repository again. And you'll see all of
our
core all of our code stored on GitHub now. So at this point, our repository is set up. And we
can
move on to deploying our application. In this lesson, we're going to take a look at the
first
of two deployment methods that we're going to cover in this course. So what we're going to do
is we're going to deploy your application to a platform called Heroku. And the reason I
wanted
to make sure to have this as one of the deployment methods was that Heroku has a very generous
free
tier. And so we can create an account for free. And we can deploy our application absolutely
for free without having to pay a single dollar. And we don't even have to provide our credit
cards.
And I wanted to make sure to add that just because I know for some of you guys, that isn't
exactly an option. And I wanted to make sure that you can still deploy your application and show off to
your
friends and family what you have created. And you'll see that Heroku has made it very easy to
deploy your application. And it's very easy to push out changes to your application. It's
a
fantastic platform. And I think you guys will learn a lot on how to actually deploy an
application by
making use of a platform like Heroku. So we'll take a look at how we can, you know, make use
of Heroku learn about the Heroku CLI, so that we can push our application to the platform.
And
ultimately, have anyone be able to access our API. Heroku has a lot of really good tutorials
on how
to deploy applications on their platforms. And they've got one for Python as well. So if we go
to Google, just search for Heroku Python, and the first result is going to be their
tutorial,
it's going to walk us step by step on how to actually deploy our application. But before we do
that, there's a couple of things that we have to do. First of all, go ahead and create an
account
on Heroku if you don't if you haven't already done so. It's completely free. So just provide
your email and so on. Then there's two other requirements we have to have get installed
on
our machine, which we've already done. And then finally, we have to install Heroku itself. So
if you go to the setup section right here, you're going to see the installers for all of the
different
operating systems they support. So just select whichever operating system you have, and get
the Heroku CLI installed on your machine. And after you get it installed, usually you have to
close
out any of your pre existing terminals and reopen them to actually be able to access the
Heroku
command. If you have VS code, a lot of times I've had to just close out VS code altogether,
not just close the terminals, but actually close out the entire application, and then open it
back
up. But after you've done that, if you want to verify if Heroku was successfully
installed,
just go to your terminal and just type in Heroku dash dash version. And if it spits out a
version,
that means Heroku has been successfully installed. And after it's been installed, we want to
log in. So we do Heroku login. And then you just press any key right here.
And it's going to open up your web browser, you can just log in, and then provide your
credentials.
All right, and then once you're logged in, you can just close out this
window.
Go back to our code and you can see logging in is done and we've logged in. And then that's my
specific account right there. Alright, and once that's done, we're going to go back to the
tutorial.
And we're just going to kind of follow along. So if you go to prepare the app, you can ignore
this. So they give you like a demo application to kind of follow through the tutorial. But
we
already have our own app. So we don't need to worry about that. If we go to deploy the app, we
have to run this command Heroku create. So we have to create an app within our Heroku
account.
So let's run that command. But before we run that, I'm going to do a Heroku create dash dash
help.
And we can see that one of the arguments is going to be the name of the app we want to create.
And so if you don't provide a name, Heroku just gives you a random name, but I always like
to
use a more familiar name. So I know that what app, which app covers which one of my
applications.
And so I'm going to call this fast API, Sanjeev. And keep in mind, these app names are
global.
So if I deploy my app as fast API dash Sanjeev, you will not be able to create an app with
the
same exact name. And that's because to actually access our application once deployed, the URL
is
the URL is going to be the specific app name embedded in there. So it has to be globally
unique.
So if it says your your app name is already taken, then just try a different one, maybe add a
couple of numbers at the end, it doesn't really matter what the name is.
All right, and once we've created our app, the next thing that we have to do is, well,
actually,
before we even do that, I want to show you guys what it actually did. So there's a couple
things that happened behind the scenes. And the first thing is, if we actually log into our
account,
I'm going to log in right here.
We're going to see our dashboard. So in the dashboard, you can see the application that we
just created. Right now, there's nothing interesting, nothing's been set up with it.
So we don't actually see anything. But the second thing that it also did was if I type in git
remote, this is going to show all of the remote, remotes that have been set up for our
Git. And we can see there's two now. So there's origin, which is the one we set up originally,
that's GitHub, ultimately, but Heroku added a second one. And so this is ultimately how
we
deploy our application, because instead of doing a git push origin main, to push it out to
GitHub,
we can just do git push Heroku, Heroku main, and that's going to push out our code to the
Heroku
platform. And then it will then create an instance for our application. So let's go ahead and
do that. We're going to do a git push Heroku main. And this is going to take a little bit of a
while,
because it's got a, you know, setup Python, it's got to do a few other things, it's got to
install all of our dependencies, before it can actually get our app up and running. So I'm going to
pause
this video, and then I'll touch base with you guys once that's complete. All right, so
our
application has successfully been deployed. And you'll see that in the logs Heroku actually
gives us the URL of our application. So I'm going to copy this URL. And we're going to just open
this
up in the browser. And if you let this run long enough, you're eventually going to see an
error.
So it looks like something in our application is broken. And this is to be expected, because
there's
still a few more things that we have to do to actually get this set up. Because if you take a
look at what we've done, we pushed out our code to Heroku, right? But Heroku has no idea how
to
actually start our application. That you know, what is the command? It doesn't it just knows
that this is a Python app, but it doesn't know what it is. It doesn't know that it's a fast
API
app, it doesn't know that we're trying to build out an API, it has no idea. And when we deploy
to our development environment, we ran uvcorn. And then you know, app dot main, and then
app.
Right, but we haven't given that command to Heroku, how does it have any idea that our code is
in main.py. And that within the main.py file, we have our app instance stored in a
variable
called app, it has no idea. So what we have to do is we actually have to create a
file
that's going to tell Heroku, what is the exact command that we want to run. So inside your
root directory, so just kind of collapse all the folders,
inside your root directory, so just right click here, create a new file, create a file called
a proc file. So I've already got this in here, it's going to be in the GitHub
repo. So you can take a look at it, and make sure you capitalize the P. And actually, if you
take a
look at the, the tutorial, and we go to the next one, which is define a proc file, this is
going
to just explain what the proc file is. But the proc file just just tells Heroku, what is the
command that we need to start our application. And so here, we give it a process type. So since
this
is a web application, that's going to be responding to, you know, web requests, we want to use
the word web. And then we specify the exact command that we need to run. And so in my proc
file,
a couple of things have been set up. So we run the came in UV corn, app dot main, and then
app, like we've done before, we're not going to pass in the dash dash reload flag, because this
is
production, we don't want it to automatically reload on changes, because there should be no
changes. We do have to provide the host IP. So this is just saying that, hey, we should be able
to
respond to request to any IP. So whatever IP Heroku gives us, this is going to accept
it.
Then the next flag is going to be the port flag. So what port should we run this on? If we
don't provide a port flag, then it's going to default to port 8000, like it always has.
However,
the thing about Heroku is that they're actually going to provide a support. So we don't know
what this port is ahead of time. So we have to be able to accept this port regardless of what it
is.
And so it's actually going to pass this down as an environment variable. So anytime you want
to accept an environment variable or reference one, you can just say dollar, and then curly
braces.
So we're really just saying, we want to take whatever value Heroku gives us with the
environment
variable of port, and we want to assign it here. And then this is just giving it a default
value of 5000 if they don't provide one. However, Heroku will always provide a support. So
I
don't think we even need that. And so after you make this file, you're then going to have to
push
out these changes to get once again. And since I already had this file, there's nothing to
change
in my gets or nothing will get pushed out. So I'm just going to just change some random code.
I'm going to delete this 1234 here. So that I can at least have some changes in my
code,
you guys will already have changes in your code because you added the new file. So then go
ahead and do a git add dash dash also that's going to add all the changes, we're going to do a
commit.
And we'll give it a message of added proc file. And then we do a git push. And then what do
we
push to? Well, first of all, you know, moving forward, we want to make sure that this is
stored in GitHub. So let's do a git push origin main, this is going to push it to GitHub. And then now
we
want to actually push out these changes to Heroku so that we can actually get our application
up and running. So we'll do a git push Heroku main. And once again, we have to kind of wait
through
this entire process. Okay, so it's now finished. Once again, we're going to go back to the
URL
it's deployed at. And I'm just going to do a refresh to see if we fixed our issue. And
the
fact that it's still spinning for this long most likely means that we did not fix our issue.
But I already know the exact reason why this is. And so we will take a look at exactly what is causing
our
application to not successfully get deployed in the next lesson. Anytime your application
isn't
working on Heroku's platform, they have a really easy way of accessing logs. So we can say
Heroku
logs, and I'm just going to do a dash dash help just to see what options we
have.
And so here we could specify the app, or we can just dump the logs for all of our apps. But
what I'm going to do is I'm going to pass in the dash t flag so we can tail the logs as things
happen.
So if I do a dash t, and we scroll up, you can see that we've got a couple of different
errors. And
so it looks like there's some issues with settings, which, you know, is the the pedantic model
that
we've set up for retrieving our environment variables. And it's saying that there's a couple
of validation errors for our settings model. And it says that, hey, look, we don't have a
database
host name, we don't have a database port, password or any of the other information. So there's
some issue with our environment variables. And this makes sense, because in development, we use
our
dot env file to provide all of the environment variables into that settings model right
here.
And we pass that by specifying the env file. However, we did not check our env file into
get
remember in get we we use the get ignore file to make sure we excluded that because we don't
want
to actually check that into get. So how does Heroku know what all of our environment
variables
should be? Well, they have a and so we actually have to add those environment variables either
through the command line, or through the dashboard. But before we can do that, we need to
actually
get a Postgres database. Because right now we don't have a Postgres database. So what would
we
even provide for database host name and port and password and things like that if we don't
have one. So let's create one. And once again, Heroku provides us with a free Postgres
instance
that we can have access to. And that's what that's one of my favorite parts about Heroku is
that now we have access to Postgres. So let's go to Heroku Postgres. I'll just say
tutorial.
And it's going to take us to their dev center. So this is going to show us actually how to
create a Postgres instance. And so it Postgres is one of their many add ons. If you want
to
actually go to their website and see all of the different add ons they support, they support a
ton. If you want to read us add on, you can get that added. But in this case, we want to create
a
Postgres, a Postgres instance. So we do Heroku add ons, create, then the type of add on,
which
means it's going to be PostgreSQL and then plan name. So we are under the hobby dev, which is
essentially the free one. And that's going to actually create a Heroku a Postgres
instance.
So let's run this. And just Ctrl C out of this. And let's just do a dash dash help real
quick,
just to see what other flags we have. And that should be fine. We don't need to actually
provide any other flags. Let's run this. And Heroku is actually going to create our Postgres
instance.
And keep in mind that for Heroku to spin up a Postgres instance, it does take a little bit of
time. But we can actually monitor that in our dashboard. So if we go back to my dashboard
and
do a refresh, you can see that this is the new Heroku instance. And I can just click on
this.
And we can see some more information is going to open this up in a new tab. And if you want
to, we can go into these settings. And here we can take a look at our database
credentials.
So if I view my credentials, you can see that this is the IP address my database lives
on.
This is the actual database name. So we don't get to create our own database name or our
own
database within Postgres, they give us a fixed name, and that's perfectly fine. And then
here's
this is going to be the username that we're going to log in as this is the port it's running
on. And then this is our password. And they also provide us the full Postgres URL if we
want
to use that. So now that we have this information, feel free to save this real quick. And I'm
actually just going to do a quick screenshot and save this in my notes so that I don't have
to
keep bouncing back and forth. And now let's go back to our application. And if we go under
settings,
this config vars is where we actually provide environment variables to our Heroku
instance.
And by the way, Heroku calls our instance dinos. But you can use either term you want. And so
you could see all of the config variables that have been set, and there should be nothing except
for
one. So when you add a Postgres instance to your application, what happens is Heroku
automatically
adds an environment variable called database URL, and it's going to contain the entire
Postgres q l, u l, u r l. And so we could go into our application and then update our
code,
so that in the database.py file, instead of, you know, breaking things down like this, we can
just provide, you know, one, one specific URL and just put it in as settings dot, you know,
database,
underscore URL. However, I don't like to change my code, I like it just the way it is. So
instead
of using the default environment variable, we're going to actually break this URL out into
multiple
different environment variables. So just open this up and take a look at all of the
environment variables that we expect by going into our config.py. So we need all of these set. So we're
going to add
one for database host name. And from the URL, if you forgot what the host name is, and
actually,
I'm going to just open this one up in a new tab, and then open up actually, I don't even need
to open up this one. I just need to flip between these two. So the host name is right here.
But
you can get it from this URL just by clicking on this icon, and then just grabbing the IP
address, but it's such a small little window. I hate having to do that. So I'm going to close
that
and just bounce between these two links. All right, so let's grab the host
name.
And we'll add this one. Then the next one is going to be the database port,
password,
name and username. So let's just write those out database or database
username,
database, password, and then database name. I think that's it. Database port, password,
name,
username. And then we have to add this stuff for our token. So we got secret key
algorithm,
and then the last one. And then now we can provide in the values of the database
port,
we can see from here, it's going to be the default 5432, the username,
username. And then here, we'll grab the password. And then the actual database name
here.
The secret key, I'm just going to use whatever we used in development, not actually
recommended,
but I'm just going to keep things as simple as possible for now. But you can provide any
secret key you want. And I'll say in production, it'll expire after 60 minutes. All right. And
so
now we've got our all of our environment variables set up. And our Postgres Postgres instance
is
already up and running. So I think at this point, our app should work. So we'll try this out
again.
So now that we set those new environment variables, how do we actually restart our Heroku
instance? Well, let's do a Heroku apps dash dash help. And let's see if there's a
restart.
So there's app create destroy. And it doesn't look like there's a restart. So we can actually
use
the apps argument to restart our application. Instead, we have to do Heroku ps dash
dash
help. So now we can see that we do have access to restarts, we're going to do Heroku ps,
restart.
And that's going to restart our dyno, which is our Heroku instance. And once
again,
I'm going to do a Heroku logs dash t to tail the logs. All right. And so now you can see
those
previous errors were gone. And we were now able to successfully run the startup command. And
you
can see that we actually got passed a specific port of 34731. And so that's why we
ultimately
had to provide the port variable because this will change every time we restart it. However,
we don't actually need to use that specific port number in the URL that we need to access. So
let's
get our our app URL, which I already forgot. And if you ever forget your URL, you can just
type in
Heroku apps info than the name of the specific app. And then it's going to give you all of
these specific information. So you can see this is actually where the Heroku Git is being
stored.
We can see that we have one instance in this case running the web. And then we have our URL
down
here. So I'm going to copy this. And you can see that we get message Hello world. So it looks
like
things are working. If I actually go to slash docs, let's test that out. And so now we have
access to
documents, I'm going to minimize that. So it looks like things are working. However, if I try
to log
in, whether it's either through the documentation, or if I actually send a request from
postman,
you're going to see that there's going to be some issues. So here I'm going to actually, I
can't even log in, I have to create a user first, because this is a fresh deployment. So let's go
to
users, and we're going to create a user real quick. And I'm going to just try this out and put
in whatever name you want. Now, so this is Sanjeev at example.com, my password will be password
123,
we'll execute it. And you can see that we get an internal server error. So what is happening?
Why
can't we exactly create a user? Well, like usual, we're going to go back to our logs. And
we're
going to see what happened. Alright, so we can see the SQL that's getting imported in the
logs.
And it looks like there's some kind of error with the SQL. And I don't know if that's going to
give us a more specific error that's going to actually help us. I think that's all it's going to
provide
us in this case. But I think you guys can guess exactly what's happening. We deployed a brand
new
instance of Postgres. But right now, if we actually connect to our Postgres, you're going to
see a few
different issues. So I'm going to actually connect to this Postgres instance. And to create a
Postgres
instance, just right click here, and then select Create server. So this is going to allow us
to connect to another Postgres. And I'm going to call this Heroku dash Postgres. And then
here,
we're going to provide the same exact connection information. So we'll go back to settings,
we'll
get our credentials. Okay, and so now we are successfully connected to to our Heroku
Postgres
instance. And if I just drop this down, and then go into databases, you'll see a ton of
different
databases. Keep in mind, we won't actually be able to access most of these. So the reason
Postgres
is able to us or the reason Heroku is actually able to provide this for free is that they have
one instance of Postgres. And then they give you one database within that instance, that you
can
access, but only you can access that specific database. And so our database is D five
eight.
And I don't know if there's an easy way to search for databases. Let me see if I can right
click this. Nope, it doesn't look like there's an easy way. So I'm just gonna have to scroll down to
D
five. And then what are the next letters D five, eight, re zero, five. Alright, so I finally
found
my database, you'll see it's the one that I have access to. So it's actually colored. And if I
just open this up real quick, and go into schemas, public, and then tables, there's nothing in
there.
So that's really the reason why our application is not working is because we have a brand new
Postgres instance, but we haven't actually set up our tables or schema. And so this is
where
alembic is going to come into play. Once again, you'll see that since we already have all of
our revision setup, getting our production database to match our development database will be as
simple
as just running one simple command. So we'll take a look at that in the next video. In our
development
environment, we used alembic to manage our database schema. So alembic was responsible for
creating
all of those tables. And since we have all of these revision files within the alembic folder,
we can essentially track all the changes that we make. And so to actually get our
development
database up to date, we just ran alembic upgrade head. And that would go to whichever one of
these
is the latest revision, and make sure that our Postgres instance matched whatever that
is.
In our production database, because in our production database, it's going to be no
different,
we're going to just run alembic upgrade head on our Heroku instance. And that's going to
ensure
that our Postgres database gets updated to our latest schema. And because we've checked all
of
these files into Git, because all of the alembic folders was added to Git as well, that then
our
Heroku instance has access to all of our revisions. And so when we run alembic, alembic can
keep track
of all of those changes as well in our production server. And it's important to understand
that we
never run alembic revision on our production server, we only run alembic revision on our
development
server when we're staging out these changes in our production server, whenever we want to push
out those changes, we just do a git push. So we push out the code, all of our code changes, as well
as
all of the alembic revisions to our production server, and we just run an alembic upgrade
head.
It's as simple as that. So let's do that. Now, the first thing that we need to do is we got to
figure
out how do we actually run a command in our Heroku instance? I think your first guess is
probably
going to be do we add another line into this proc file that we can kind of call whenever we
want to? Not exactly, although maybe you can do that. But if we go back to our tutorial, and go to start
a
console, we can run a specific command on our Heroku instance by doing Heroku run and then
the
specific command that we want to run. So let's try that out. And you can even drop into the
bash shell.
So there's a lot of flexibility. But we're going to just do a Heroku run. And then we want to
provide
the command that we want to run. I'm going to put it in quotes, I don't remember if it's a
requirement to put it in quotes. But I'm going to do that just in case. So we'll do alembic upgrade
head.
Alright, and you see that it's running that command right now.
And take a look at the logs, right, you can see that not only did it get our Postgres instance
up to the latest revision, it has all of the other incremental steps added in as well. So we can
roll
back to any of them, just like we did in development. And if we go back to PG admin, I'm going
to just
refresh tables. And if we go to our tables, you can see that we have all of our tables here
now.
And just in case, because I believe we crashed our application, maybe, when we try to, you
know,
create a user without having a Postgres database or table setup, I'm just going to do a Heroku
ps
restart again, just to restart the instance just in case. And now I want to go back to here.
And
we're going to just go back to the root URL to see if that works. And that works. That's
perfect.
We'll go back to docs. We can see that that works. Now let's try to create a user. And so I'll
do
try it out. And I don't even feel like changing any of these values. Technically, these should
still
work. So let's just try that. And we got a response back. And look at this, we got a
201.
And we can see the user that was created. So it looks like our application was now
successfully able to access our database. If we go into the users table, right click on this and just do
a
view edit all rows. We can see the one user that we created. Alright, and so that's pretty
much it
we've successfully deployed our application. The last thing that I want to show you guys is
how do
we push out changes to our application? Let. So let's say that I go into my app. And right now
I
go into main.py. And I see that this just says message Hello world, and let's say I make
some
kind of change. And let's say I just add a whole bunch of exclamation points or something just
keep it just keep it simple, just to show you guys, you know, regardless of what change you
make,
the steps are going to be identical. We make some changes. The next thing that we have to do
is,
we actually have to push out those changes. So first of all, you're definitely going to, you
know, push out those changes to your GitHub repository. So we'll do git push
origin main. So that's going to push it out to get GitHub.
Well, sorry, I forgot to do a git add dash dash all so we actually have to add those changes
and then commit those changes. So I'll do git commit. Then we'll do a git push origin main,
that's
going to push it out to GitHub. And then finally, to actually push those changes out to our
production environment, we'll do a git push Heroku main.
All right, and so that's pushed out now. And if we go back to our
application,
I'm going to go back to the root URL. And we can see that we now have the extra exclamation
points.
So we have successfully pushed out our changes. All it took was just one simple command. And
keep in mind that if you make changes to your code, that's all you have to do.
However,
if you make changes to your database, so you know, you go into alembic, and you create a new
revision, then we have to follow the same exact steps. But on top of that, we have to do the the Heroku
run
alembic, you know, upgrade head so that we actually push out those changes to the Postgres
instance.
But there you guys have it, you have successfully deployed the application that we have spent
building the past couple of days. And so feel free to show this off to your friends and
family.
Feel free to make as many changes and modifications really start to play around with it. Try
to expand
on the functionality of our application. And just keep learning, I guess. For the second
method of
deployment, we're going to see how we can deploy your application on an Ubuntu server. And
it's
important to keep in mind that it does not matter where you host your Ubuntu server, you can
run it
on any of the major cloud providers, AWS, Azure, GCP, you can run it on DigitalOcean, you can
run it
on your local machine, if you have virtual box installed on your computer, you can run it on
Raspberry Pi, it doesn't matter, the steps are going to be identical as long as you run it on
an
Ubuntu server. Now, what we're going to do in this course is I'm going to show you guys how to
deploy it on DigitalOcean, just because it's $5 a month, it's cheap, it's a fixed price. If
you
deploy it on AWS, then the price is kind of it varies. And if you run it all, all month long,
then
it could spike up well past $5. So this is kind of the cheapest solution. But keep in mind,
like I
said, the steps are identical, regardless of where you deploy. I just want to focus on making
sure
that you guys understand the steps that needs to take place to actually set up your Ubuntu
server to be ready to host your application. And so if you don't have a DigitalOcean account, go
ahead
and make one. And then once you log in, you'll see this window. And what we want to do is
select
get started with the droplet. So we'll select that. And what we want to do is we want to
select
Ubuntu 20.0.4. That's the latest version at the moment. If you're watching this in the
future,
you could probably grab one of the newer ones, most of these commands are going to be
identical. But if you wanted to make sure that you don't run into any issues, select the same exact
version.
Then for the CPU options, you want to select regular Intel with SSD, that's going to be the
cheapest. And then you want to select the cheapest one right here, which is $5 a month, or
0.007
per hour, what is that 7.7 10th of a cent. All right, and then select your data center. So
I'm
just going to select whatever's closest to me, I'm on the East Coast. So I'm going to select
that.
And then nothing else, for the most part really matters, we can leave everything as default.
And then you have two options for authentication, you can use SSH keys, if you're familiar with
how
to work with that. If you're not, then start out with password for now, it's just a little bit
simpler. And you'll see that DigitalOcean has some strict requirements for passwords. So
you're
probably going to have to try this a couple of times before you find one that actually matches
their criteria. And then here, we can just give this a specific host name, I'm just going to
call
this Ubuntu dash fast API. It's nothing more than really just a tag. And that's all we have to
do
is select create droplet and DigitalOcean will create our Ubuntu VM. All right, and once
the
loading bar finishes, you should see a public IP. This is the IP that we're going to use to
connect
to our virtual machine. And so we'll take a look at how to connect to this in the next
video.
To connect to our Ubuntu virtual machine, we need to open up our terminal. So just search for
terminal
on Windows. If you're on Mac, you can do the same thing, just search for terminal. And then on
Windows, you can select command prompt, I installed another terminal. So I'm actually going to
use
that. But you can use this one as well. You can even use the built in terminal within VS code,
it's all going to be the same thing. Okay, and to connect to a device, we have to use a
protocol
called SSH. So we do SSH. And then we have to provide the username that we want to connect
to
to the device. And so this is going to vary depending on where you're deploying your virtual
machine. On DigitalOcean, this is they're going to create an account for the root user. So this
is
like the most privileged user, this is where you can ultimately do anything with the device
you have unlimited access. So we're going to do SSH root at, and then you do the add symbol, and
then
we provide the IP address. So I can just hover over this, select copy, and then paste it in
here.
At this point, you'll get this, you know, fingerprint message. So just hit Yes. And then it's
going to ask you for the password. So this is the password that we provided when we
created
the virtual machine. And at that point, we are now successfully logged in. And so you can, if
you
run LS, this is just going to print out the contents of your current directory. And so it
looks like we've got one folder in our directory right here. But this is our Ubuntu virtual machine. And
anytime
you you get a brand new Ubuntu virtual machine, the first thing that we want to do is we want
to update all of the installed packages. So we want to make sure that it's up to date, right? This
is
just like installing any other operating system. And to run that there's two commands that we
want to run. So we do sudo. So this is going to give you well, technically, we don't need to run
sudo
because for the root user, but if for some reason, you're not the root user, then you always
have to use the keyword sudo. So we do sudo apt update. And then the second command, we can do two and
signs
and say sudo apt upgrade. And then pass in a dash y flag. The reason we pass in the dash y
flag is
because if you don't pass in this flag, what's going to happen is it's going to do a few
things and then a prompt will come up where you actually have to hit yes. And I always hate having to
hit
yes, when you pass in the dash y, you can let it run. And then you can just go do something
else, go get some coffee because it is a fairly long process. If you get this pop up, it doesn't
really
matter what option you choose, you can say keep local version currently installed or install
the package maintainer's version, I'm going to select this one, but it shouldn't make a difference.
So
once all of those packages are up to date, we need to install a couple of extra packages. And
the
first thing that we need to do is, well, let's actually check what version of Python we're
running on this machine. So if I do Python, dash, dash version, all right, so it doesn't look
like
Python's installed. However, if I do a Python, three dash dash version, we could see that
we
actually do have Python, it's just we have to use Python three, because a version three of
Python
was installed. And so if you see anything that starts at 3.8 dot 10, or later, you should be
good.
However, if you want to, you can always do a sudo apt install Python, and then I think you
could just
do a dash and specify the specific version that you want. But you can you can figure out how
to do that by taking a look at the Python documentation, 3.8 dot 10, or later is more than good enough
for
us. So we have Python, the next thing that we need to do is we need to install pip. And let's
actually double check and make sure that it's not already installed on our machine. So we could
just
say pip, dash, dash version, we could see there's no pip here. However, let's try to pip three
dash
dash version. And it looks like we don't have pip installed, but it's giving us a little hint
on how to install pip. So we can do sudo apt install Python three dash pip. And we'll hit Y for
yes.
And once that's done, we're going to use pip to actually install a virtual NV, because we are
going to make use of virtual environments on this machine as well.
So we'll say sudo pip three, install virtual NV. And so now we should be able to use the
virtual
environment command. The next thing that we have to do is install Postgres on this machine. So
we'll
run sudo apt install. And then the libraries that we need are Postgres ql and Postgres ql
dash
contrib. And then we'll do a dash y so that we don't have to hit yes.
Okay, so we've got Postgres installed, how exactly do we connect to it? So what we're going to
first
do is before we try to connect to it from our local machine, I want to connect to
it
from the actual Ubuntu computer itself, the Ubuntu virtual machine. And so we're
actually
going to take a look at the the CLI for accessing the Postgres database. And so the tool that
we use
to do this through CLI is called psql. Right, and we should be able to have access to it. So
if you
do psql dash version, you should see that it should respond back with something. So that means
psql
is installed. And if I do a psql dash dash help, it's going to show us the different flags
that we
can pass in. But the main flag that we want to pass in is the dash u so we can specify what's
the username that we want to connect to. So let's say psql, then dash capital U, and then our
username.
And we know that when we install Postgres, it's going to install a user on the database
called
Postgres. So that's what we always use to connect to it by default. And so we say Postgres.
And if you want to, you can specify the specific database you want to connect to that believe there's a
flag
it might be dash D. Yep, the database name. So you could technically say dash D Postgres,
but
Postgres is going to be the default. So you can leave that off. And we're connecting to our
local
host, which is going to be the default as well. So we don't need to pass in that. And the
port, we can also pass that in the default is 5432, which is what it's running on. So we don't really
have
to touch anything else. And we're going to try and connect like this. So let's see what
happens.
And we get this error, it says a psql error fatal peer authentication failed for user
Postgres.
So for some reason, it's saying peer authentication failed. And this is a special meaning,
right? It
sounds like a regular login failure, but it's not. And it's because Postgres when you install
it on Ubuntu, it has a special way of authenticating users by default. So when it comes to
local
authentication, and what I mean by local authentication is the Ubuntu VM connecting to the
database itself. That's what a local authentication means. Whereas if I'm connecting
from my Windows machine to the Postgres database on this, on this Ubuntu VM, that is not
called
that is not considered local authentication. So since we are on the same machine that we're
trying to connect to our database, Postgres considers this a local authentication. And
on
Ubuntu, we have a special configuration called peer authentication. Now this is the
default
configuration for Postgres. And what peer authentication does is it actually takes the user
that's logged in to the Ubuntu machine, and it tries to log in as that user
essentially.
So even though we pass in the dash u flag, since I am technically logged in as the root
user,
right, Postgres is going to error out because it will only allow a Ubuntu user called Postgres
to be able to log in as the user called Postgres on the Postgres
database, little confusing, right, but it actually connects, it actually obtains the username
from
the Linux kernel. So whoever you're logged in as that's the only person you can log in as. And
so what we can do is first of all, because this is the default configuration, Postgres
understands
that so Postgres actually created a user on our Ubuntu VM called Postgres. So if you want to
see
the list of all the users on your Ubuntu machine, you can do a sudo cat slash etc slash path
wd.
And you can see all of the users that have been installed. And you can see that it actually
created a user called Postgres on this machine. So I'm going to change that user so we can do
that
by doing Sue dash and then the name of the user. So Postgres. Right and notice how we changed
to
this specific user. And so now if I try to do p SQL dash u Postgres, we can now log into
the
database. So if you see this, that means you're successfully logged into the Postgres
database. And while we're logged in here before we do anything else, because first of all, we're
going
to get rid of that peer authentication because I hate it. We want to create a password for
our
Postgres user. And to assign a password to this user, we can say,
backslash
password, and then the name of the user that we want to assign a password to. So we're going
to use the Postgres user. And then we're here, we're going to provide our password. All right. And
so
now we should have a password. And I'm going to exit out of here. So to exit out of the
Postgres
console or terminal, whatever you want to call it, you just do backslash, and then Q for
quick. And
then we're still logged in as the Postgres user. And so if you type in exit, it's going to
take us back to the root user. There's going to be a few more things that I want to change to our
Postgres
installation. And so anytime you want to modify the configs for Postgres, what we want to do
is
we want to move to a different directory. So the command to move to a different directory is
CD. And then we specify the path to the directory. So we want to go to slash etc. So
generally,
any application to install on Linux or at a bunch of machine, usually the configs for
that,
that package or that application is going to be in slash etc. And then here, I'm going to say
Postgres slash. And then if I hit tab, you can see that it's going to auto complete. So
you're
going to specify whatever version of Postgres you ran. And so in this case, I have Postgres
12. But
if you installed a version 13, then you would put in it would be a folder called 13. And then
we
will go to main. And if I type in LS, we'll see all of the files that we have in this
folder.
And the two main files that we want to focus on our PG underscore hba conf. So this is where
we configure Postgres, as well as Postgres ql.conf. And so we're going to open up Postgres
ql.conf.
So I'm gonna say sudo vi, Postgres ql.conf. And we want to kind of scroll
down.
And we want to take a look at the connections and authentication section. And so if you take a
look at the default configuration, we can see that under listen addresses, it's only going to
allow
localhost to connect to our database. That means only when we're logged into this Ubuntu
VM,
can we then log in to this Postgres database, it is not going to allow me to take PG admin on
my
Windows machine and connect to it remotely. And so I want to change this. And technically, you
don't have to do this. But you know, if you do want to be able to connect to it
remotely,
then you want to change these configs. And so I'm going to put this config, we're going to
say,
listen, underscore addresses, equals. And then here, you would provide the IP
addresses.
And so, you know, if you wanted to only be able to connect to this database from, you
know,
your, your office, then you would put the IP address or domain name of your
office.
If you want to allow all IP addresses to connect to it, you would just do a star. So that's
what I'm going to do. It's going to allow me to connect to this regardless of what computer I'm
on,
or where I am on the network. However, it generally when it comes to best practices, you want
to really narrow down the scope of the different IPs and domains that can access your
database, because that's the most important part of your application, you want to make sure
it's as secure as possible. Alright, so we'll save this, that's the only change we need to make in
this
file. Then the next file that we need to change is PG underscore HBA comp. So we'll do the
same
thing to the VIP GHBA comp. And we're going to scroll down. And we're going to take a look
at
our default configuration. So I did mention that when we connect locally, it uses a
peer
authentication. So you can see that for local connections. And I forget what the second
column
is, if we actually scroll up, I got to go up, actually, let's see what the second column is.
Okay, so this is going to be the format of it. So this is going to specify what type of
connection,
then what database this connection should be associated with, or this rule should be
associated with, what's the user and then what's the method? Well, we also have address and method if you
are
connecting remotely. So we want to change this. So we want to say for local users, regardless
of,
you know, what database they connect to, and what user they connect as, we want to make sure
that we do not use peer authentication, we use regular password based authentication, which is MD
five
in this case. And so here, we're just going to change this. So this is saying, for any
database,
this rule is going to apply for the user Postgres, we're going to say, instead of peer, we
want MD
five like we have here. We'll say MD five. And we're going to change this one to MD five as
well.
And then lastly, for these two sections right here, these are going to be for remote
connection. So if
I want to connect from my Windows machine, this is the rule that's going to apply. So this is
saying that for you know, hosts, for any database, any user, right now, the only IP addresses
that
can connect to it are going to be 127001, which means localhost. So that's effectively
blocking
all remote connections. So I'm going to go here, and we are going to change this. And I'm
going to
say, zero dot zero dot zero slash zero. So this means any IP address. And then just to keep
things
nicely formatted, I'm going to move that over there. And then we're going to do the same thing
for IPv6, which is what this line is doing. And I'm going to say this, so this is the syntax
in
IPv6 for saying any IP address. And that should be all of the config changes that we need to
make.
And so I'm going to exit out of here and make sure to save it. All right. And at this
point,
you probably think the changes have been made. However, generally, when you change any
configuration file, you actually have to restart the application for the changes to take effect. And so to
restart
an application, you do system CTL, restart, and then the name of the application. So this is
going
to be Postgres ql. All right, and it should be running at this point. So let's try this out
now.
Right now, we are still logged in as the root user. So remember, before we actually had to
change to the Postgres user on our Ubuntu machine before we can log in as the Postgres user. So now
instead,
I'm going to do p SQL dash u Postgres. And I want to see if I'm able to log in. And so now I
actually
get a prompt for a password. So it looks like things are working so far. So I'll try my
password.
And now we are successfully logged in to our Postgres database. That's perfect.
Now,
let's actually try to connect to it using PG admin. And so PG admin, I've got my local
machine,
which is right here. If I want to add a new machine, I'm going to right click on servers,
we're gonna select create server. Give this a name, I'm going to call this fast API dash
prod.
Call it whatever you want. Connection details. Here, we're going to provide the IP address. So
I already forgot what the IP address of our machine. We logged in 13412 to whatever that
is.
We're going to provide this. That's default port. That's the the user we're going to connect
as.
And then if we want to, we can put in a password here as well.
All right, and now we are successfully logged in to our production database. If I open this
up, take a look at the databases, we just have the one default database that always
gets installed. Till now, we've done everything as the root user. And it's generally frowned
upon in
the Linux world, regardless of what flavor Linux using it could be Ubuntu, CentOS, Red Hat,
you
generally don't want to be logged in as the root user, because it opens the door for
potential
security vulnerabilities. And it opens the door for you to potentially break your machine. And
so what we're going to do is we're going to create another user, this user is going to have
root
privilege. So he'll still be able to make any of the changes. But it's better than being
logged in as the root user. And more importantly, when we actually install our API, our Python
application,
we're going to have our non root user be the one responsible for starting it, we don't want
the
root user to start the application, because then we're giving our application root access to
our machine. And that is very risky. So instead, we want a non root user to start our
API.
So on Linux to create a new user, you type in the command user mod. Sorry, not user mod, we do
add user, and then the name of the user. So pick any name you want. I'm
going to do Sanjeev in this case, but you can name it like fast API or whatever you
want.
And then it's going to ask you for a password. So put whatever password you
want.
And then you can provide your name, but none of that really matters. And just hit Y here. And
so we've now created a user. And what we can do is if you want to,
you can do a sue dash and then Sanjeev. And then it's going to move you to the Sanjeev
user.
Or what you can do is you can exit out of this connection, and then exit out of here
altogether. So now we're back on our local machine. And instead of when you do an SSH route to add
whatever,
you can change this to the new user that you just created. And so that's going to log you in
directly as that user. Now this user, by default doesn't have root access. So if I tried to do, you
know,
pseudo apt upgrade, right, it's gonna ask for pseudo password for Sanjeev. And I'll put my
password
in and take a look at the error that we get. It says Sanjeev is not in the sudoers file, this
incident will be reported. So anytime you create a user, you have to give him
pseudo access or root access so that he can actually perform operations that require root
privilege. So we're gonna have to exit out of here. Once again, I'm going to log back in as the
root
user. And to give a user root access, we use run the command user mod. And we pass in the
flag
dash a and then capital G, make sure it's capital. And we say sudo and then the name of the
user. So
I created a user named Sanjeev. So I'll just put that in. And so at this point, the user
Sanjeev
has root access. So let me exit out of here again. And we're going to connect back as the user
Sanjeev.
And so now I'm going to run sudo apt upgrade. And so if I try that, put my password
in,
you can see the command run. So we now successfully have root access. If you type in PWD, this
is going to tell you the exact directory that you're in. So I'm in a folder
called slash home slash Sanjeev. So whatever your username was created as, right, it's going
to your
default home directory is going to be slash home and then your username. And so while you're
in your home directory, and if you don't know how to get to your home directory, you could just say
CD,
and then tilde. So that's going to take you to your home directory, or you can do CD slash
home,
and then your username that's going to take you here. Whoops. All right, so once you're in
your
home directory, what we're going to do is we're going to create a folder for our application.
Technically, you can put this anywhere on your machine, I'm just going to put it in my
home
directory. So I'm going to call this, this folder app, or we could call it fast API, it
doesn't really
matter. They'll say MK dir, this is the command to create an app, sorry, create a directory, I
call this app. And then to move into that app, we do CD app. And within this folder,
what we're going to do is just like we did on our local machine, we're going to create a
virtual environment. So I'm going to say a virtual nv. And then we'll call our virtual environment v
nv.
Alright, and then if I do an LS minus LA, let's just make sure I created the virtual nv
folder,
then to activate our virtual environment, we'll type in the command source. And we go into the
that folder, the bin folder, and then the activate file. All right, and then we should see this
little
nugget right here. So this is going to let us know that we are currently in a virtual
environment. And if you ever want to get out of the virtual environment, you can just say
deactivate.
Within our app directory, we're going to create another folder called source. So this is going
to contain all of our source code. So we'll CD into the source. And now what we're going to
do
is we're going to copy all of our code onto this machine. And so how do we do that?
Well,
since we already have it stored on GitHub, all we have to do is just go to the URL for
our
repository. So this is my repository that I created. This is the main one called fast
API
dash course. This is public. So any of you guys can access this, but you're going to go to
your specific repository. And once you get to the repository, this is going to be the
URL,
or you could just select code right here. And then this is going to give you a little link to
copy. So we can just copy that. And then we can go back here. And we can say get
clone,
paste that in here. And then before you hit enter, hit space, and then dot. So that's going to
install
in this current directory. And it's not going to create another directory. That's the name of
my
repository, which is kind of annoying. I don't actually want a directory called fast API
course.
So we'll hit that, we'll run it, it's going to clone it. And at this point, if I do an LS, we
should see all of our specific code. Great. Now, before we do anything else,
let's reactivate our virtual environment. So we'll say source, the envy,
lib,
or not live bin activate. All right, we've got our virtual environment set up again. And we're
going to move into our source directory. And if you recall, remember, we don't have
any
packages installed. So we don't even have fast API installed. But we do have our requirements
dot txt
file. So if I just cat that real quick, if you forgot what that was, that's going to tell this
machine exactly what version of every single dependency we need to install for our
application
to work. And so we can just install all of those with one command by doing pip install dash
r
requirements dot txt. All right, so I got an error. And this was expected on my end, because
when I was running
through this, I ran into this error. And so if you see this error right here, you know,
anytime you see red, it's never good. But what you want to do is you want to go right about
here
where it says include and then lib pq dash fv dot h, this is implying that we're missing a
package
that's probably called lib pq. If you actually search this error, you're going to get the same
result. So if you run into this exact issue, or even another issue, just look for the
library
mentioned right here, or it says that include line, right and just search for that library and
just see how to install it. Okay, so it's since it said we're missing this library, I'm going
to
do a sorry, it's not actually a pip install, we actually have to do an app install. So we do
sudo apt install. And actually, before I even do that, I'm going to deactivate my virtual
environment.
And we're going to do a sudo apt install lib pq dash dev. Alright, so now that we've got that
library
that missing library installed, we're going to then go back into our virtual
environment.
And we're going to run that same command to install all of our
requirements.
And I have to move into my source folder.
All right, and it looks like we have successfully installed all of our packages. The next
thing that I actually want to do is let's go ahead and try to start our application.
So we have access to uv corn. So we can just do a uv corn app.main. So this is the same
exact
command that we ran on our local machine and then app, we don't need to pass in the reload
flag,
because this is our production server. So we would never want it to automatically reload on
changes, because no one should be making changes on our production server. So we'll run this and
let's
see what happens. And it looks like we get eight validation errors, which is to be
expected,
because it looks like we are missing all of our environment variables. And that's correct, we
never set our environment variable. So in the next section, we're going to take a look at
how
we can set up our environment variables on our Linux machine. Okay, so let's get started
on
creating our environment variables. So what we want to do is to create an environment
variable
on a Linux machine, we say export. And then we give the name of the variable here, this is
just
a demonstration. So I'm gonna say my underscore name. And then we set equals and then
whatever
value we want to give it so you can give it anything. I'm just gonna say this is and jeeve.
Alright, so we'll hit that we'll run that. And then to actually see if this worked
to see all of your environment variables, we just say print n v. And so this is going to spit
out
a whole bunch of default environment variables. And then to see the one we sent, just kind
of
look down, look for my name, and we could see that it was successfully set. So at this point,
we could just go one by one, and manually set all of these. However, that is a very
inconvenient
process. And I wouldn't expect you guys to do that. Because, you know, we've got seven or
eight, and
any other project could have 10 2030. So it's not really realistic to do that, you know,
one
environment variable at a time. So let me quickly remove this environment variable, which you
can do with the unset command. And so I could just say unset my underscore name. And if I do a
print
n v, we should see that it's no longer there. And what we're going to do is I'm going to go
back to my home directory, because I don't want to mess around in my application directory, I'm gonna
go
CD, and then the tilde, or you can do slash home slash, and then whatever your username
is,
she's gonna do that. Alright, so this is gonna take me back to my home directory. And I'm
going to create a brand new file. And if you want to create an empty file,
you can just say touch, and then I'm going to just call this dot e and v. All right, so we
shouldn't do an LS minus LA, we should have that dot e and v file right here. And I'm
going
to open that up. And here, I can provide a list of all of the environment variables. So I can
say,
and keep in mind, this has to be in the same exact syntax. So we have to say just like we
would put
on the CLI, we'd say export, then the name of the variable. So I'll say, you know, my name
again,
she goes Sanjeev. And then let's maybe let's do my underscore password,
which is going to be password 123. So I can save this file. And I can say,
source,
dot e and v. So this is going to set all of those environment variables. So if I do a print n
v now,
we do have my name. And somewhere along here, we've got my password. So that's another
way
of setting things. It's a it's a little bit easier to kind of keep everything in a file. But
what I don't like is that I have to put it in this format where I put export and then
the environment variable, what would be awesome is, if I could just somehow get it so
that
the environment variable file in my Ubuntu machine matches with the environment variable
file
on my local machine, I want to just be able to put the the key and the value pair like
here.
And so that way, I could just copy this, paste it into my production server and just change
the values there. And so we can actually do that, it just requires a little bit of a
modification.
So let's go back here. And I'm just gonna unset both of these real quick. And
we'll
just double check it got removed. So they're removed. And we're going to open
up
the dot e and v file. And I'm going to just delete everything. And what I'm going to do is I'm
actually just going to copy this for now, and just paste it in there.
Let's just see if I can get all of this set from a file. And then we'll change the values
later.
And so when you have a regular file, like the one we have right here, where we don't have the
word
export in front of it, it does require a little bit more of a complex command. But all you
have to do is you run this command. So you do set dash oh, all export, then source. And then you
provide
the path to the file. So this is going to be my home directory. So it's slash home slash
Sanjeev, just make sure you swap it out with your specific username. And then we provide dot e and
v.
And then we say set plus, oh, all export. All right. And so now if I do a print and
V,
we can see I've got my database password, my database username, I've got my database
port
host name, and all of the variables that I set have now been set as environment variables.
So
that's what we're going to do. However, there's still one minor issue. If I exit out here,
or
actually sorry, not if I exit if I reboot. And I actually have to do a pseudo to actually
reboot,
but I'm going to reboot. And I'm going to show you guys that when you reload your machine,
you'll lose your environment variables. So we'll give this about a minute or two to kind of
boot
back up. Although I find a bunch of machines boot up fairly quickly. So let's just give this a
shot,
see if we can connect. No, not yet. All right, so I've logged back in. After it
rebooted,
if I do a print and V, you're going to see that all of the environment variables that I set
are all now gone. So how can we get all of these environment variables to persist through a
reboot?
Because if our machine goes down, comes back up, then we lose those environment variables,
then our applications broken. Well, there's a couple of different ways that we can
have
environment variables persist through a reboot. One of the ways is let's go to our home
directory.
We're going to do an LS minus LA. And what I'm going to do is we're going to go into this
dot
profile file. And we're going to set that exact command that we had run to set my
environment
variables. Where is that command? This one right here in that file. So copy this. And then
just do a vi dot profile. And then go to the bottom of the file. And below the last
line,
just paste that command in. And then we can just wq. All right, and so this is going to cause
any
time we open up a session or a terminal, even through a reboot, it's going to run that
specific
command so that those environment variables are all set. Right now, if you do a print
nv,
you will see that it's not set. And that's to be expected, because we were already connected
to it. But if I exit out of here, reconnect, do a print nv, we can now see that the
our
environment variables are set. And then just to prove to you that this actually persists
through reboot, we're going to reboot this machine. All right, so it's rebooted, let's do a print
nv.
And we can see all of our environment variables are still set. So this is the method that I'm
going
to go with. Keep in mind when it comes to environment variables, there are 1000 different ways
to tackle this. And so people like to set their environment variables in different
ways.
This is just the way that I've done it. This is the simplest way. But there are, you know, a
number of different other methods, some of them will probably be more recommended, better. But
I
want to keep things as simple as possible. The one thing I do want to kind of highlight is
that
our environment variable is stored in my home directory, it is not in the app directory
where
all my application code is. And that's important to me, because I don't want to put it in my
application code, because I don't want it to accidentally get checked into get I don't
want
anyone to accidentally see my production passwords or anything like that, because then my
entire
application is compromised. So I like to keep it separate by moving it into a completely
separate folder, so that there's no chance that it gets mixed up in my application code. All
right,
so since we can connect to our Postgres database, let's go ahead and actually create the
database within this machine. So I'm going to select right click here, and I'm going to say create
database,
call your database or whatever you want, I'm just going to call it past API to keep things
simple.
And so now we have our database once we click on it. And so now it's up and running. And so
since we have that information, and we have the information for connecting to our actual
Postgres
instance on this machine, let's update these environment variable files, so that they
are
accurate at this point, because I believe they're still pointing to whatever our production
was set as. Sorry, our development environment. So here, the host name is still going to be local
host.
The port since I installed Postgres on the default port, we can keep that the same. You want
to make sure you update your production password, I chose the same exact password,
just to keep things simple. The database name, we just created one called fast API. Here, this
is going to be the database username, if you want to, it is recommended to create
another
Postgres user. However, I'm just going to keep this as the default Postgres user to keep
things
as simple, you definitely want to upgrade, update the secret key, the algorithm, you can leave
that
as default. And then the access token, I'm going to make this a little bit longer. Let's do
300 minutes.
All right, and now let's reactivate our environment. So I'm going to go into
apps.
And then we'll do source vnv bin activate, that's going to activate our virtual
environment.
And right now, you probably notice a little bit of an issue. And that is that we don't have
any
tables right now, we go to our schemas. And then our we got no tables in here. Oh,
sorry,
we're in the wrong database in the fast API, we've got no tables, right, nothing
here.
And so we have to set up all of our models now. However, since we are in our production
environment,
and since we've set up alembic, you'll see that this is dead simple. And when it comes to
alembic,
it's important that in your production environment, you never create revisions in your
production environment, you only create it in your development environment, and you check it into Git. And
since
we checked it into Git, if we go to source, and then go into our alembic file folder, and do
an
LS and then go into versions, we have all of our revision files. And so what we can do is we
could
just ah, sorry, I didn't mean to hit exit got logged back in now. All right, make sure that
virtual environment enabled every time I'm going to go into source. And what I'm going to do
is
I'm going to do alembic upgrade. And then head. So this is going to set up our database and
it's
going to upgrade it to the latest revision. All right. And take a look at that. It created
all
of the individual revisions one at a time. So we can, we can still roll back just the same
exact way that we did in development. And with just a click of a button, we've got our entire
database
schema all set up. And we can just refresh this real quick, and then go into our tables. And
you
can see that we've got our three tables plus the alembic table as well. So now that we have
our database all set, I think it's time we try to start our application again. So move into
your
apps directory. And if you've already, and if you haven't activated your virtual environment,
do that
again. Move into the source directory. And now we're going to run the same uvcorn command that
we
ran in our development environment. All right, so it says it's running on the local host IP
address
of this machine on port 8000, which is the default port. So let's try to connect to it. So
what we
have to do is grab this. And we're just going to paste the IP address into our browser. And
we're
going to do colon port 8000. And let's see if this works. And it looks like it didn't work. So
what
happened? Well, the problem here is that we needed to make sure that it can listen on any IP
address.
So when you just set this to 127 dot zero dot zero dot one, that means only this machine can
access
it. So I'm going to Ctrl C out of this, so we're going to stop it. And we're going to pass in
one
more flag, we're going to say dash dash host, and then provide the IP of 0000. So we're going
to
listen on any IP that this machine is potentially running. And if you wanted to, if I did a
dash dash
help real quick, we can also specify what port we want to listen on. So if I do dash dash
port,
I can provide a different port. So it doesn't go to the default port 8000. But I'm going to
keep it as port 8000. You're going to see that it ultimately doesn't matter when we
actually
deploy, because we're going to deploy in a way that it's always going to use either port 80 or
port 443, which is the default port for HTTP and HTTPS. So this is good enough. We're going to
start
it again. Now you can see it's listening on any IP. And we're going to refresh. And if you
guys
can't see that, because it's tiny, we can see that I got message Hello World, which is
accessing that root path that we have. So it looks like things are working at this point. However, right
now,
you know, if we, if this program crashes, or if we reboot our machine, you're going to see
that it
doesn't actually restart automatically. And so what we're going to do is we're actually
going
to use a process manager. So we're going to use something called G unicorn. So control C out
of
that, instead of just starting our application with uvcorn, we're going to use G unicorn. And
if you don't, if you haven't already installed it, which you probably haven't, do pip
install
G unicorn. And make sure you're in the virtual environment. And so G unicorn is going to
be
responsible for starting our application. Now, I've already got this installed, because I
added it to my requirements dot txt file a little bit earlier. But if you haven't done that,
that's
fine, because I haven't told you guys to do that. So just do a pip install G unicorn. And if
you get any errors, most likely, you'll need two other packages, you'll need pip, just so you'll
need
HTTP tools. And so you can just do a pip install of HTTP tools. And you'll also need one
extra
library, which is UV tools. So if you so if you hit any other errors moving forward, right,
go
ahead and install those two libraries, HTTP tools and UV tools. And that should fix any errors
that
you have. So those were some potential errors that I ran into when I was kind of doing a quick
test run. But at this point, we should have access to our to a G unicorn command. So I can say G
unicorn.
And let's just do a quick quick dash dash help. Just to see what options we have. And
here,
what's nice is that you can actually set the number of workers. So you know, if you have if
you're on a multi threaded machine, or you have a virtual machine with multiple CPUs, you could
set
up more than one worker so that they're all listening all at once. And then it's going to get
load balanced across all of them. So we're going to set up for by default, just to show
you
you could set up one, it doesn't really change anything. So the command is going to be G
unicorn
dash w. So this is for the number of workers, we're gonna say I want four workers. And then
we're going to say dash k. We're gonna say uvcorn dot workers dot capital uvcorn.
Capital
worker. Alright, and then now we just specify how to start our application like we normally
do. So
we go into our app folder, we grab the main file, and then we start app. And then, you know,
just
like we did with uvcorn a little bit while ago, we had to start it with the dash dash host,
and then
do all zeros. Here with G uvcorn, sorry, G unicorn, we have to use a dash dash bind, and then
we do
zero to zero to zero. And then we specify the port we want to listen on. Let me drag this
so
it looks a little easier to read. So we do that. And then the port that you want to listen on.
Let's start that. And it looks like I got a couple of errors. So let's see what
happened.
Alright, and so if you get this error, UV loop, it means we're missing a library. And so like
I said,
if you get any errors, make sure you do the pip install HTTP tools, I should have this one
already installed. And then you want to do a pip install UV tools.
And I realized I've been making a mistake, it's not called UV tools, it's going to be UV
loop.
Alright, and so now it's successful. So not not UV tools, UV loop, I guess I got it mixed up
with
HTTP tools. And so you can go ahead and if you actually wanted to, if you did a pip
freeze,
you could go ahead and copy this and make this your new requirements.txt file that you check
into
Git. This is going to have G unicorn automatically in there because we already installed it.
It's
going to have UV loop. And it's going to have what is the last one that we needed HTTP tools,
which is right here. And so you can check this version into Git. And so that way, it's all
up
to date. And so when you deploy to other Ubuntu machines or other computers, it's going to
have all of these extra packages so you can deploy right away. All right, and now let's run
the
same G unicorn command. And now it looks like it started. It says it's listening on this
port.
And notice something interesting. Look at this, it actually started up for worker nodes. And
you can even see the process IDs. So this is probably the parent ID. And then these are the
little
worker IDs. So if you wanted to, we can open up another connection. And I can do a ps dash
aef
pipe, grab, we could probably just grab for you, G, G unicorn. Yep. And so you can see all of
the
different processes that were started. And so there should be five of them 12345. One of
them
is the parent process. And then the four are the other child processes. All right, now
that's
working. Let's just do a refresh just to make sure we didn't break anything. And it's still
working. And so if we do a refresh, you can see that we can still access our application. And if I go
to
slash docs, the documentation should still be there. Perfect. So it looks like our app is
working,
we can test it now, by testing the individual endpoints. But we're going to hold off on doing
that, because we're going to make a few more changes. So the first thing is, right
now,
this is, this is running on our specific terminal. And so it kind of locks up our
terminal,
I want this to be running in the background. And more importantly, I want this to
automatically
start up on boot. Because right now, if we reboot our box, right, we'd have to manually log
into
this device and run the same command. So we have to find a way to kind of automate it so that
upon
reboot, it's going to run the application and we want it to run in the background so that we
don't see any logs by default. And we'll tackle that in the next video. Alright guys, so in this
video,
we're going to figure out how we can have this command automatically run upon reboot, as
well
as how can we run this as a background process so that we don't hang our terminal. So what
we're going to do is just do a Ctrl C to stop it for now. And we're gonna what we're gonna do is
we're
going to create our own service. And if you ever run system CTL, and then you know, either
status
or start, or, or restart, and then the name of some of some application that you wanted to
run,
you know, that is an example of making use of a service. And so what we're going to do is
we're going to create we're going to make our own service, which starts this application here for
us.
And you'll see that it's actually pretty simple. If we go to slash at C slash system D. And
then
system, and I forgot to do a CD to move into that directory. If you do an LS, these are all of
the
services that have been installed on our machine. And we're just going to create a brand new
one.
So how do we create that we have to create a couple of files. And I have actually created them
for you guys ahead of time. So if you check my GitHub repo, you will see a file called g
unicorn
dot service. And there's going to be a couple things that we have to change. So let me
just
walk you through what each one of these lines mean. So under unit here, we're just going to
give you a description of what this is going to do. So this is going to be a G unicorn
instance
for to server API, you can put whatever description you want, you can say the demo fast
API
application doesn't really matter. After now, the next line right here, where it says after
network
dark target, what this is going to do is this is going to tell system D or Ubuntu
machine,
when to actually start this service. And so it's saying that, hey, we need our network service
running before we can start our API. Because if the network's not up, then nothing else
really
matters. So we're going to wait for network to come up before we start the service. Then on
the service, we're going to figure out we have to specify what user is going to, you know,
run
this service. So we need this to be associated with the user, I created a user called Sanjeev,
so I'm going to update this. And then for group, you want to just put the same exact name as
the
user. Then we have to specify the working directory. And so this is going to be the
directory,
our service is going to be launched in. So go ahead and update this to point to the directory
your app is running in. And if you don't know what that is, remember, just go home, then
move
into apps. And then you can go into the source folder if you want, and just type in PWD.
So
that's going to get you the exact path that you want. And so I have to update this with my
username.
And then the environment. So because we want this service to run the G unicorn
command,
which is right here, we needed to run in a virtual environment. So to actually set the virtual
environment through this script, we have to say environment equals and then we set
this
path variable. And we just point it to the exact path to our bin folder. So once
again,
you don't know how to get there, just go to your app folder, go into your virtual
environment,
go into your bin folder, and then just type in PWD. And that's going to get you the
path.
And so I just have to update this with my username. And you'll just do the same thing, you
just have to update the username, then we have to specify the exact command to run. So
we
have to provide the path to where the G unicorn command resides on. So it actually sits in
the
bin folder. So if you actually do an LS here, right, you can see the G unicorn command. And
so
that's going to give it the path to the command. And then after that, well, actually, I have
to
update my username once again. And after that, it's the same exact command that we ran
for.
So for workers dash k, then we have this long command right here. And then we specify how
to
start our application, which is this command, and then we're going to bind it to zero to zero
to zero. And then you can pick whatever port that you want. And this is just a default
config,
don't worry too much about it, you need it, but I don't want to go too far into how Linux
works to actually explain this, just know that this is just a standard procedure for creating a
service.
So to actually create the service now, what we do is let's go to slash Etsy, system d, then
system.
And let me deactivate my, we don't need to deactivate it's fine. We're going to create our
service file here. And so I'm going to do a pseudo vi. And then the name of the service.
So
what do you want to call your service? So when you type in system, CTL, and then start, what's
the
name of the service you want to type in, you get to specify, I'm going to make it so that it's
just called API, very simple, you can call it fast API, you can call it social media app, it doesn't
really
matter, it's up to you, but I'm going to call it API. And since I want to call it API, I'm
going to create a file called API dot service. And then what we can do is just take this copy it, and we
will
paste it in here. Alright, and to test out our service, let's do a system CTL. Start
API.
And we're gonna have to put in our password. And so this started our API. And if we do a
system
CTL status API, whoops, there shouldn't be a space CTL status API, they should just give us
the status
of our service, we can see that looks like it failed, that's not good. And it's going to
provide
us some logs. And if we take a look at the logs, you can see algorithm field required access
token
expires field required. So it looks like something is wrong with our environment variables,
because
we saw the same exact error when we were originally setting this up before we got the
environment variable. So for some reason, it's not actually accepting these environment variables. And
the
reason for this is that the service file, the service that you create doesn't have access
to
users environment variables. And if you actually go back to our home directory, and then we go
to
LS, LS minus LA. Remember, we created this ENV file, and then we loaded it in the dot profile.
So
the environment variables that get loaded from dot profile aren't accessible
within
a service. And that's just the way services work. So what we have to do is we have to pass in
another
field in our service config, so that it knows to pull the environment variables from that
specific
file. So what we can do here is in this file, we're going to go and just add another
line,
it doesn't matter where you add the line. But we want to say environment file, make sure
to
capitalize the E and the F equals and then you provide the path to your specific ENV file.
So
mine's going to be home slash Sanjeev dot ENV. And I'll just copy all of this
again.
And we'll go back into Etsy system D system. And then I'll just do a pseudo VI API dot
service.
And we're going to replace this file now.
Alright, so now we should be able to import all of those environment variables. And you
there's
another way to do this, you can also set up a config file for your, your new service.
However,
I figured it's just easier because we already have our environment for our dot ENV file. So we
can
just continue to use that. And now if I do a system CTL restart API, and notice you don't
have
to type in API dot service, it's optional, you can you can technically do it like this. And it
looks
like since we actually changed the file, we have to run this command now. Alright, and so now
let's
do a system CTL restart. Sorry, not restart status API. And uh, looks like we got the same
exact
error. Let's just do a restart one more time. And we'll do a status. And now it's up and
running.
Perfect. Look at that. So we've got our four workers. These are the process IDs. And let's
just
double check and make sure that we can access our specific website. So I'm going to just
refresh on
the slash docs. And it looks like it works. Let's go to the root. And let's go to the root
URL.
I'll just change this to the root URL. And see if we get the API response. Perfect. So it
looks like
we've got our application up and running. There's just one last thing we need to
do.
And I wasn't sure if I had already covered this. But there's one last thing I want to do. And
that is I want to make sure that our service automatically starts upon a reboot. So that way, you know,
in
case our machine goes down, whether it's because of a power failure or something like that, or
maybe
we perform some sort of upgrade, I wanted to automatically start this service so that our
application is always up and running. If you do a system CTL status API, you can find out
pretty
easily whether this service is going to get started upon a reboot. So if you take a look right
here
under loaded, and then right here is the keyword that you're looking for, if you see disabled
right here, that means that it's not going to automatically get started upon a reboot. If you see
it's
enabled, that means it will automatically get started upon a reboot. So to actually make sure
that it actually does get started upon a reboot, just do a pseudo system CTL, enable and
enable
then the name of the service, which is API. And after that, if we do a status, again, we
should
see that it moved to enabled. And so now if we reboot our machine, and you guys can go ahead
and test this, you'll see that it automatically starts up the service. Right now, the way that we
have
our deployment setup is that when we send a request to our server, you know, to the specific
IP address of our server, and whatever port we decided to configure our app to listen
on,
we send that request directly to the app or G unicorn in general. And this is
technically
perfectly fine. However, you'll see that in more professional type deployments, what we'll
have is
a intermediary web server. So we'll have a web server running on our Ubuntu machine. And
that
web server will receive the original request and then act as a proxy and proxy that request to
our
specific application or G unicorn in this case. And so one of the common web servers that can
be
used for that is nginx. There's a couple others like traffic and HA proxy, you'll see that
nginx
is fairly popular, though. And some of the benefits of doing this, or configuring
our
application ultimately to be deployed like this is that nginx is optimized for SSL
termination.
So nginx is a high performance server, and it can perform all of the SSL termination. So when
we
when we send an HTTP s request, we can send it to our nginx server, the nginx server can
process it
and then forward it as a plain HTTP packet to our application. Because technically, we
could
configure our application to handle HTTPS, it's not optimized for that. Right? Our, our app
is
designed to just act as a plain API, when we start adding responsibilities like performing
SSL
offload, well, then you'll see that we start to see a degradation in performance. And
nginx,
like I said, is a high performance web server, it's optimized for this. So it makes sense to
kind of handle that responsibility on nginx instead of our application. So that's what we're going
to
do, we're going to configure nginx. And this is going to act as the gateway into our system.
So any request we send to our system, whether it's HTTPS, or HTTP doesn't really
matter,
we're going to send it to nginx. And if we send it as HTTPS, what nginx will do is it'll then
take it
and send it and forward it as an HTTP packet to our application. And so I'm going to walk
you
through how to set up nginx. It's not difficult, we do have one config file that we have to
add. But outside of that, it's fairly easy to get this up and running. Let's install nginx. Now to install
a
package, we can do sudo apt install, and then nginx, and then dash y. For that installed
nginx,
if we do a system CTL, start nginx, that's going to start up the nginx service. And so now if
you
go to your server IP address, you're going to see that you get this welcome message. So this
is just
the default page that nginx loads up for you. And we're going to change this. So in nginx,
what we
want to do is we want to go to slash etsy nginx sites, and then available, it's important
that
you do available because there's also a site enabled, which is not what you want. You want
available. And there's this default config file, or technically, this is a server block. And if
we
take a look at this, you can see the default configuration. In this case, just to show
you
what that looks like, you can see that this is handling HTTP traffic. That's why it's port 80,
it's going to listen on port 80. And this is going to act as the default server. So all
requests
that come here are going to land on this server block. And we've got the same thing for IPv6
as well. And then this is the file that you actually see. So when you navigate to this page,
it's
actually loading up a file that's in slash bar slash www slash HTML. And then the server
name,
so this is going to be the actual domain name that's going to handle requests for this is
basically saying everything. Everything else, don't worry too much about that. But what
we're
going to do is we're going to change that file a little bit. And I've provided an example file
here. And so this is what an example file looks like, we can leave pretty much everything
in
place. The only thing I want to change is this location block. And so this is what is going
to
allow nginx to act as a proxy, specifically this line, this is the main line, you don't
technically
need the other ones, those are more of optimizations. But all this is saying is that any
requests
that match this, the the root path or beyond, essentially, is going to get proxied to local
host
port 8000, which is what G unicorns running on. So you just specify the IP address and the
port
that you want to proxy traffic for. And so since G unicorn was configured to run on port
8000,
we're just going to forward it to G unicorn in this case. So I want you to just copy these
lines here. And we're going to go in. And I'm going to just open up that default file
now.
And we can delete these three lines.
I'm just gonna paste that in. And at that point, I can save this.
And then we want to do a system CTL restart nginx. So it's restarted. And if I refresh this,
right, we can now see our API. And if I go to slash docs,
it should also work as well. So we got nginx setup. And running the last thing that we have to
do
regarding nginx specifically, well, it's not really nginx specific, but in general is
that
right now, if you take a look at it, you can see our web browser says not secure. And that's
because I can actually copy this. I paste it, right, we're using HTTP, and not HTTPS. So we need to set
this
up for HTTPS. So how do we do that? Well, we're going to tackle that in the next video. But
you're
going to need a domain name if you want this to work with HTTPS. So I'll show you guys how to
kind
of grab a cheap domain name. However, if you don't have one, don't worry, you can just leave
it as
HTTPS for now. Or you can create a self signed certificate, whichever one works for
you.
For my application, I purchased a domain name called Sanjeev dot xyz. So if you've ever
watched
any of my other videos, this is always the default domain name that I use for demonstration
purposes.
You can see that I actually bought a dot xyz because they're the cheapest if you just want to
play around just for learning purposes, I get a dot xyz, they're usually like $1 for like
an
entire year. But if you get a dot com, it's gonna be a little bit more expensive. However,
depending on you know, what your domain name is, it at most is still well, not at most, but you can still
find
plenty at $12 per year. So it's not too bad. And I happened to purchase my domain name from
names
cheap. So this is my domain registrar. However, there's plenty of other options if you want to
use GoDaddy, if you want to use any of the other any of the other websites, you can even use
Amazon
if you wanted to, feel free to use whichever one you want, you just need to get a domain name,
I'm going to be using this one moving forward. So just keep that in mind.
And what we want to do is we want to set up a couple of rules. And we actually have to point
our domain name to digital ocean and digital ocean has this excellent write up how to point
your
digital ocean name servers from common domain registrars. And they actually show you how to do
this for a lot of the main domain registrars. And they have names cheap here as well. So if you
get
one of these, you can just follow along. But you'll see that you know, any domain
provider,
you'll be able to kind of relatively click around and just kind of navigate yourself and
figure out what's the equivalent gets for the most part, the UIs are all fairly similar. But if we go to
down
to names cheap, you can see that what we have to do is under the name server section, we have
to do custom DNS. And then we point it to the DNS servers of digital ocean. So n s one
digital
ocean calm and as to n s three. Let's do that. And I've actually already set that up for us.
So you
just go under name servers, select custom domain, custom DNS. And then you have n s one
digital
ocean calm n s two digital ocean calm and n s three digital ocean calm. And there'll be a
little checkmark. So just check that there. And at that point, it's going to point everything to our
digital
ocean servers. Then in digital ocean, if you just select your name here in your
dashboard,
what you want to do is you want to go to manage DNS on digital ocean. And then here you want
to
enter your domain. So this is going to be Sanjeev dot XYZ. We'll add this domain. And the
first
thing that we have to do is we have to create a record. So this is the root of your domain. So
I'm just going to say at symbol, this is going to create an A record. And we want to select
the
specific droplet that we created. So this one's a bunch of fast API, you should only have
one.
But if you have other droplets, make sure you select the right one, and then select create
record. And at this point, if we do Sanjeev dot XYZ, in our web browser, it's going to take
us
to our digital ocean server. However, keep in mind, the changes to DNS usually take a
little
while to propagate. So you could potentially have to wait up to an hour or two hours names,
cheap says it could take up to a date, but it never takes a day. So if you don't see
these
changes immediately, just give it, you know, 1015 minutes, and then you'll see that they work.
But I'm going to add one more. And that's going to be for my subdomain, because I don't want
to
just do Sanjeev dot XYZ. I also want to redirect WWW dot Sanjeev dot XYZ to my server. So I
can
create a C name record. And I can say WWW. And so you can see what this is going to look
like.
And I'm just going to say at symbol right here, that's going to point it to this record,
essentially. Alright, and so what this is, what this is saying is that if you send a
request
to Sanjeev dot XYZ, it's going to send it to the IP address of our server. And then if you
send a request to the subdomain of WWW dot Sanjeev dot XYZ, it's going to be an alias for
Sanjeev
XYZ. So it's essentially just going to follow this. Alright, and so now if I go to Sanjeev dot
XYZ,
you can see that we are successfully able to access our API. And if I go to slash
docs,
once again, this should still work as well. And that's pretty big. Let me minimize that. And
if
I do the same thing for my subdomain of WWW dot Sanjeev dot XYZ. This should also work.
Alright,
so we've got our domain set up. And once we have our domain, we can now finally set up SSL. So
we
can handle secure HTTPS traffic, because we don't want to send our application traffic over
HTTP.
And we want that nice little lock symbol, that green lock symbol. So we'll tackle that in the
next video. Okay, so it's now time to set up SSL for our application. So I want you to search
for
certified. So this is the website for let's encrypt, which is a free SSL service. And
here
they've got excellent documentation. What we want to do is select to get certified
instructions. And then we can just specify what software we're running. So this is for nginx. And the system
is
going to be a bunch of 20 dot 0.4. The reason it asked for this information is that it
actually will automatically reconfigure nginx for you to handle HTTPS, because normally when you want
to
set up HTTPS, you have to change some configs and nginx, I think it's a little complicated.
And so the it's saying the first thing that we have to do is we have to install snapd.
If you installed a bunch of, you know, from DigitalOcean, snapd should automatically
be
installed for you. So you can do snap the dash dash version, I think. Or is it snap dash
dash
version? I think it's snap version. Yeah, okay, so it's snap. Okay, so we've got snap
installed, that's perfect. And then what we want to do is we want to install with this command right here.
So
just copy this. And we're pretty much just going to follow this website, because I told like I
said, the instructions are fantastic. So we'll install cert bot. Right, cert bots installed, then
we
want to do this command. But you have two options, you can either just generate a
certificate.
Or you can have cert bot automatically configure nginx for you, we're going to have cert bot
do the work for us. And so we can just paste this in. And then we have
to provide an email address. So this is going to be whatever email you want to use. And then
we
got to agree to the terms of service. You can do no for this one. Right, then we have to
specify
what domain names we want to generate a certificate for. So we want to generate a certificate
for Sanjeev dot XYZ, in my case, and then www dot Sanjeev dot XYZ. All right, and it looks like
it
did some magic to our default config file that we were the we were originally changing. So if
we
actually go to the sites available folder, which we're already at, and just do a cat
default,
you're going to notice some changes to this file. And that's because cert bot actually made a
whole bunch of changes. And the main change is where where are these changes? Not here, not
here,
here we go. So this server block is for SSL configuration. And so these are the
changes
they made. So we're going to listen on port 443. So that's the default SSL or HTTPS port.
That's
good. And I guess that's for IPv6. This is for IPv4. These are going to be the two
certificates that were generated. And then there's a few other configs that it added. And we also have
this
server block right here, which is going to actually redirect HTTP requests to HTTPS. So
we
can never even use HTTP anymore. It's going to ensure that our clients always use HTTPS
and
no redirect them to HTTPS. And you can see that right here, you know, if host equals
www.sanjeev.xyz,
we want to send a 301 redirect to HTTPS, and then host request right here. And we're doing the
same
thing for just the Sanjeev dot x, y, z. And so that's all done by cert bot. And so now if I
go
here, and I do Sanjeev dot whoops, dot x, y, z, take a look at this, we get the little green
lock,
that means connection secure, that means we're using HTTPS. And you can see the HTTPS here.
And if I hard code it to be HTTP. Look at that, it still has the lock because it redirected to
HTTPS.
And let's just make sure that the docs work now. And it looks like the docs still work. So we
now
have HTTPS setup on our nginx server. And just like we did with our service, we want to make
sure that
nginx has been set up to automatically start upon a reboot. So we could do a system CTL status
nginx.
And let's just quickly verify if it's if it has been set up to do so. And I can see that on
mine, it's set up to be enabled. So it will automatically start upon reboot, which is what we want. And
I
think this is the default configuration when you install and set up nginx. However, if you see
disabled here, just go ahead and do a system CTL, enable nginx. And that's going to make sure
that
it will be moved to an enabled state. So it'll get started upon a reboot. The last thing that
we
need to do to complete our deployment process is set up a firewall on our machine. And the
reason
we want to set up a firewall is just for basic security purposes, we want to make sure that we
only open up ports that we're going to be using. Because right now you can access this box on
any
port. And we want to make sure that we really harden our system so that only the traffic
that
should be sent to this box is allowed to this box. We don't want people trying to connect to
different services on our on our machine that they shouldn't be. So we're going to be using a
the
built in firewall called a UFW. And if we do a pseudo UFW status, UFW is the firewall. And
so
you can see right now it's inactive. And that's perfect. And what we have to do is we have to
set up a whole bunch of rules. And we have to tell the firewall what kind of traffic should we
allow
to our machine. And we can run pseudo UFW allow and then we can specify, you know, what
specific
port we want to allow traffic on what specific IP addresses should we should be able to talk
to. So since this is a public API, we want to make sure that technically anyone can access our
server
from HTTP and HTTPS. So we're going to do allow HTTP. So this is going to allow HTTP
traffic.
And you can see it created an IPv4 and an IPv6 rule, we want to allow HTTPS
traffic.
And keep in mind, we have to SSH to our machine so we can access it. So we have to allow SSH
as well.
And if you wanted to, you could also allow 5432. So that's going to be to allow
postgres
traffic. Now, the way we've set things up, you don't need to actually allow this
port.
Because technically, our app, right, our fast API is running on the local machine. So it's not
going
to be hindered by this firewall. So it can access the database because it's running on the
local machine. So if you wanted to make it so that no one from the outside world can directly access
your
database, you would not want to allow this. And that's what a lot of people do, because it can
be a little risky opening up your database port, because then people can potentially access
your
database. So it's up to you. However, if you want to be able to, you know, go to PG
admin,
and if you want to be able to connect to your box, remotely, then you're going to have to
allow
5432. So it's just up to you, it's your preference, do whatever you want to do, I'm going to
allow this just for demonstration purposes. So I can show you that, hey, when you allow 5432, we
can
connect remotely. So I'll add that rule. And then finally, we have to start our
firewall.
We do sudo UFW enable. And it's just going to tell us that we could potentially lose
connection
because we're starting up a firewall, that's fine. And we've started up a firewall. So if I do
a
sudo UFW status, you can see that it's active. And you can see all the rules that it's put in
place.
So it's saying we allow port 80 traffic from anywhere, we're going to allow that port
443,
we're going to allow from anywhere. And same thing with SSH and 5432, and all of the IPv6
rules as
well. And then everything else is dropped and blocked. And let's just double check that we can
access the docks. And it looks like we can and just to make sure it's not cached or
something
like that. Still works. And now we've got our firewall. And the firewall will start
up
automatically. So you don't actually have to configure anything else. If you do want to delete
a rule, you can do sudo UFW, delete, allow, and then whatever rules you can do HTTP,
or you can do 5432, whichever one you want to delete. But now that we have our
application
deployed, one of the common things that you're going to have to do is make changes to
your
application and then push out those changes. So I'm going to show you guys how we can do this
in a manual process. Ultimately, if you're developing an application, it's best to set up a
very
efficient CI CD pipeline. But that's a little outside of the scope of this course. So I'm
going to just show you how to do this manually. And you'll see that it only requires a couple of
commands.
So go ahead and make your changes, you know, whatever code changes. So I'm going to say,
Hello, world, couple exclamation points. All right, we'll save that. And we're going to do is
we're
going to do a git add dash dash all again. And we're going to do a git commit dash m give it
some
meaningful message, changed the world. All right, and then we're going to do a git push. And
before
we do that, if you just type in, yeah, just do a git push, then we provide origin, which is
the
name of our remote, and then the branch that we're on. So we can say main. And that's going to
push
it up to our GitHub repository. And at that point, once it's there, you can just quickly
verify if
you want. But I'm going to assume that it's there. And then what we're going to do is we're
going to go into our application folder, which is in our home directory, then we go into apps, and
then
source. And then here, we're just going to do a git pull. So it's going to pull all the
changes that were made. So it's going to grab the latest changes. And we'll Alright, so we've pulled
out
all of our code now. And keep in mind, you know, if you had changed the requirements dot txt
file,
and you definitely want to do a pip install, you know, pip install, dash r requirements dot
txt,
if you did, but we didn't change any of that. So we don't need to do that. And what we can do
now is we can we have to restart our application. Because if I just refresh this, you can
see
nothing's changed, there's no exclamation points. So we'll do a system pseudo system CTL,
restart
API. And let's try this out now. And we've got all of the exclamation points now. So
that's
how you push out changes through a manual process. But like I said, it's best to set up a CICD
pipeline.
In this section, we're going to focus on Docker rising our application. I originally wasn't
going to do anything on Docker, but I figured I would just make a quick video, just to show you
guys
how we can quickly set up our fast API environment within a Docker based environment. And I'll
also
show you guys how to set up a Postgres database within Docker. This is going to be a fairly
quick
run through of Docker, I'm going to assume that you guys have a basic understanding of Docker,
I'm going to assume that you already have Docker installed on your machine. So we're going to
get
right to it and get started on setting up our fast API within Docker. So I had Docker hub
open,
which is just, you know, a public repository of different images that we have access to, you
can create an account if you want to, you don't have to create one at this stage,
the only time we'll need to create an account for this course, is if you want to push an image
up to a, you know, a repository hosted on Docker hub. If you don't want to do that, then you don't
need
to go ahead and create an account. But what we want to do is we want to figure out a
image
that we're going to use for our fast API, a Docker container. And so just go ahead and search
for
Python. And what we're going to do is we're going to use the official Python image. So select
the official Python image. And you'll see that there's a couple of different options. So you
can
scroll down, you can see the different versions of Python. And we're actually going to use
version 3.9.7 in this case. But you could see that with the default Python image, we have all of
the
different versions that we could pretty much ever want. And then they got slim images, they
got the Alpine images as well. But we're going to use just the standard image. Now, if we use this
specific
image, the problem with this is that this is just a basic image with Python, there's nothing
else on
it. And at that point, if we just have a basic Python container, then we have to do all of
the
work of copying our source code onto the Docker container. And then we have to install all of
our
dependencies and set up our application. And that kind of defeats the purpose of Docker,
because the
main idea behind Docker is that we actually create an image that has everything that we need
to run
our application. So we just start up the image, or I guess we technically start up the
container, which is made from the image, and that should just have everything it needs. And it
should
just start working right away. So that's exactly what I want to do. And to actually get that
to
accomplish, we're going to have to create our own custom image. And all custom images need to
start
off with the base image. So we're going to start off with this base Python image. And we're
going to customize it by doing a few things, including copying our source code into it, and
installing
all the dependencies, and setting up a few other details. So to actually create a custom
Docker
image, we're going to go to our project directory. And we're going to create a file that's
referred
to as a Docker file. Now, this Docker file is going to have all of the steps necessary to
create
our own custom image. And the first thing that we have to do is we have to set up or specify
our base image. So we're going to use this Python 3.9 dot seven image as the base image. So what we
do
is we say from, and then the name of the image. So this is the Python image. And then if we
want to,
we can also do a version. So I'm going to do 3.9 dot seven. If you're wondering where I'm
getting
this from, this is all from the documentation, you just say from the name of the image, which
in this case is Python. And then you can see the different tags. So if you want 3.9 dot seven
slim
bullseye, you got that. And if you want to see them going over that in an example, we're
kind
of basically just following this essentially. The next step is going to be an optional
step,
but I always like to do this, we're going to use the workator flag. And this is going to set
the
working directory. So I'm going to say this is going to be a user source slash app. Now how
do
I know that there's a folder called user slash source, I've actually run the Python image
myself, and I checked to see where I could actually set up my application. So I found that there is
a
path here. And then I want to create a folder called app. And this is where I'm going to store
all of my application code. And once again, you can take a look at the documentation. And it
does
the same thing. So if you ever lost, just take a look at the doc, and it's going to help
explain things. But like I said, this is the optional command in this case, because what it really
does
is it's going to tell Docker, this is where all of the commands are going to essentially run
from.
So it's like doing a CD into slash user slash source slash app. And then anytime we give it
any instructions, it's always going to do everything in that directory. So it's just going to
help
simplify some of the later steps. But it is technically optional. The next thing that I want
to do is I want to copy our requirements dot txt file from our local machine onto our
Docker
container. So we use the copy command. We say we want to copy requirements dot txt. And then
we
have to specify the directory within our container or image that we want to copy it to. So I'm
going
to say dot slash. So where does this exactly point to? Well, remember, we set our working
directory.
So everything is going to be with regards to that directory. So when I say the current
directory, that means this directory. Now, if we didn't have this line right here, then we would have
to
specify the full path to that directory. So you can see that it's kind of simplifying things
for
us. Because then we can just kind of say, hey, I want to use the current directory, because
that's what the working directory is. And the next thing that I want to do is run a command, which is
going
to be responsible for installing all of our dependencies, because we have our requirements dot
txt file copied into our Docker container. So we're going to do a pip install the dash
R,
and then requirements dot txt. And if you want to, we can also specify the dash, dash, no,
dash,
cash, dash, if you wanted to as well. And so that's going to install all of our
dependencies.
Then the next thing that I'm going to do is I'm going to do copy dot dot. So what does this
mean,
this is going to copy everything in our current directory. So all of our source code, and
then
it's going to copy it into the current directory in our container. So what is the current
directory? Well, that's the one that's set by work there. And once again, if we don't have this
defined,
then we would have to specify the full path of slash user slash source slash app. Now, one
thing that you might notice that's a little unusual is the fact that we copy all
of our source code at the end right here, which is going to include everything in our
directory,
including the requirements dot txt file. So you might be wondering, well, why exactly do we
copy just the requirements dot txt file up here? Well, this is a little bit of an optimization
that
doctor that Docker provides us. So when Docker runs, when we build images from a Docker
file,
it actually treats each one of these lines or steps as a layer of the image. So kind of
builds
an image by running the first layer, then creating the second layer, the third, fourth and
fifth layer.
And it actually catches the result of each step. So this is important, because when you catch
the
result, if nothing changes, then we can just use the cash results. So let's say I, you know,
run
this image, or I create an image from this file. It's going to take a certain amount of
time,
because it has to, it has to do a pip install requirements dot txt, where it's installing
all
the dependencies, which does take a decent amount of time. But if I run this again, right, and
nothing
changes, well, then it can just use the cash result, because it knows nothing's
changed.
And this is important, because when I change my source code here, maybe like my database.py
file,
it's going to see what steps changed, right? And so if I change my source code, does that
change the base image? No, we're still using the same base image. Does it change the
working directory that we used? Nope, still the same. Did it change the requirements dot txt
file? Nope, still the same. Did it change any of the dependencies that we installed? Absolutely
not.
It only changed the final step. So this causes Docker to be able to use the cast result of
step
one, two, three, four, a step four, and it only has to rerun step five. But if we change
requirements dot
txt, this file right here, then it knows that this step changed here. So it's gonna have
to
rerun this step, then this step, then this step. Right. And so this gives us an advantage,
because
anytime we change our source code, we only have to run this last step. And if we change
our
requirements at txt, then we have to run all of these steps. And keep in mind that this step
is the longest step. And so if I had actually like, if I can comment this out, so if I didn't
have
this line, and instead, I copied all of my source code right here, what would happen is, every
time
I changed any code in any of my files, I would have to rerun a pip install every single
time.
And that would cause us to have to rerun the longest step in our build process. And so
that's
why we have this optimization. That's why we copy the requirements at txt file so that we can
cast that result. And so anytime we make any changes to that file, sorry, anytime we make any
changes
to our source code that's not in that file, we don't have to rerun a pip install. All
right,
and then finally, we have to give it the command that we want to run when we start the
container. So we'll say CMD brackets. And so the command that we want to run is uvcorn app dot main
app.
And so the way we actually specify the command is we, we break out each word of the command.
So here
we do uvcorn. And then we say, app dot main app. And then here, we're going to pass in the the
host
flag and the port flag like we did on our Ubuntu machine. So we do dash dash
host
000. And we do dash dash port.
That's gonna be 8000. Okay, so it's basically anytime you have a space between the
commands,
you just split it up into a different string within this list. That's just kind of the syntax
that you have to use with these Docker files. But it's the same thing as running uvcorn app dot
main
app, and then dash dash host dash dash port 8000, 8000. So it's just going to essentially run
this
in our container, we just have to provide it in this format. Right. And so now our Docker file
is complete, and we can actually build our image. So what we're going to do is I'm going to do
a
Docker, and then the command to build our images build. And then we'll do a dash dash help. So
we
can see all of the different flags. And most of these don't really matter. The only one I
really
care about is a tag. So I'm going to give this a name. So I'm going to say Docker build dash
t,
I'm going to give this image water a name that I think is reasonable, I'm just going to call
this fast API. And we have to pass in one last argument, which I forgot, which is going to be the
the
context, you can think of the context is just kind of the directory where the Docker file is.
So it's going to be in the current directory, because we can see the Docker file is in the root of
our
folder structure. So I could just say dot. And so now it's actually going to build
our
our image. And if I do a Docker image LS, we can see all of our Docker images. And you'll see
that
I've got a whole bunch. But we've got my fast API right up here. And so now we can go ahead
and use
this image for actually building out a container. And we can run a whole bunch of Docker
commands
like Docker run, and then specify the image image that you want to use and then pass in all
the flags. But I don't like doing that. I don't like working with Docker run. Instead, I want to
use
Docker compose. And if you have never worked with Docker compose Docker compose, it does the
same thing as running Docker commands on the CLI, we just provide all the instructions on a file
so
that we don't have to remember all of these long commands. With all of these extra flags, we
literally write out the flags in a file. And then we can just say Docker compose up and it's
going
to bring up all of our containers. So we're going to take a look at Docker compose in the next
video and see how we can get that set up. Alright, so we're going to learn about how we can use
Docker
compose to automatically spin up our containers with the desired configuration. And you're
going
to see that it's so much easier to use Docker compose than just running Docker run whenever
you need to, because we can spin up multiple containers all with one command. And we can
also
tear them down all with one command. And to use Docker compose, we have to provide the set
of
instructions using a file called Docker dash compose dot YML. And so this is a YAML file.
And
if you don't know too much about YAML, it's just a, I guess, technically, like a markup
language, I guess. But the most important thing to understand is that spacing matters. So this
is
kind of like Python, where you need to make sure that the tabs and the spaces are just right,
and everything lines up, or then it's going to start to throw errors.
And the first thing that we need to do is specify what version of Docker compose we want to
use. So we say version. And I'm going to say just version three, it's not the latest,
it's just come most common one. If you actually go to the Docker documentation for compose
file, you'll see the different versions that you can specify. And you can actually take a look at
the
documentation and see what feature was introduced in what version so you can figure out what
version you need to perform all of your operations, we're not doing anything special. So version three
is
just fine for us. Then within Docker compose, we have a concept of services. And so a
service,
really, at the heart of it is nothing more than just a container. So if you want Docker
compose to spin you up a container, you have to define a service. If you want Docker compose to spin
up
four containers, we're going to have four different services, pretty simple. So we have to
give each
service or each container a name. So we're going to have one container for our fast API. And
I'm
just going to call this container API or the service API. And the first thing that we need
to
do is we have to specify either the image that we use. So I could technically say like image,
and then we can point to like the Python image, or I can specify build, where we can essentially
do
the same thing that we did before in the previous video, where we ran a Docker build, and
then
provided the location of the Docker file. But we can just specify that here within Docker
composed
that Docker compose will automatically build the image for us if it doesn't exist. So we'll
say Docker build, and then we specify the context of the path to the Docker file, which is the
current
directory, based off of where the Docker composes. So they're in the same directory. So I just
say current directory. And then we have to specify a port that we want to open up because this is
an
API. So the outside world needs to be able to, to reach our container, but by default, all
containers
can't talk to the or sorry, by default, that the outside world can actually talk to your
container. So we have to open up or poke holes on our local machine to open up some ports. So we say
ports,
and this expects an a an array of ports. And so there's two different ways to do an array.
With
YAML, or with a Docker compose file, we can do, you know, just an array like this, where we
just
provide a list of things. Or we can also do it like this, where we do a dash and then
provide,
provide, you know, something and then a dash for the next entry in the list. So I'm going to
use this syntax because I kind of like it better. But to open a port, there's a specific format
that
we're going to use. So what we say is there's gonna be two numbers, there's going to be the
port on local host. And then we do that. And then we do the port on container. All
right,
so what's essentially happening is we're telling Docker, if we receive traffic on our local
host
on this port, this port specified here, we're going to forward it to this specific port on the
container. So the second number is a little bit easier to set up because it's going to be
whatever
port our applications running on. So if we take a look at our Docker file, we can see that our
container is going to be listening on port 8000. So we want to set this second number to be port
8000.
And keep in mind, you could choose to use any other value. So if you decided to use port 80
here, then you would just change it to port 80 here. But I'm gonna leave it as 8000 just
for
simplicity sake. Then the second number that we're going to use is going to be the port on the
local
machine, or on the local host, which in this case is my Windows machine. But if it's on like a
production server, it's going to be whatever machine or IP of that specific servers. And so
here,
we could specify any ports, if you can literally pick any port that's open. So if I said port
4000, what that means is that, you know, if I go to my my postman, this would be the equivalent of
doing
local host colon 4000, right, which is that first number. And so anytime we send traffic to
that
specific port on a local machine, it's going to route it to port 8000 in our container, which
is
what port our applications listening on. But if I change this to port 8000 as well, then our
request
would be local host port 8000. So I always like to have the two numbers match up because it's
just
simpler. But sometimes that's not an option. So just pick a open port on your local host
machine,
it doesn't really matter what it is, just make sure that you remember what it is. And I'm
going to leave it like that. And I'll delete this line right here. And for now, I think this is
good
enough. And what we're gonna do is we're going to try to run this and see what happens. So
I'll say
Docker dash compose up. And then if I do a dash dash help, we can see some other flags that
we
may need. And the only flag that I really need right now is the dash D. So this is going to
run the containers and in the background so that we're not automatically connected to
it.
So I'll do dash dash, sorry, just dash D.
Right. And so you'll see, if you take a look, what's happening is we're rebuilding our image.
So because we specified the build right here, it's going to build our image.
And then it's going to start our containers. So you can see that it's creating fast
API,
underscore API underscore one. And so the the naming syntax comes from the project
directory.
So it's called fast API. Alright, so that's why the first word, then we do underscore, and the
name of the service, so I called this service API, then it's API. And then it has the
number one, because there's one instance. But if I told it to spin up four, then I'd have 123
and four. So we've got our container. If I do a Docker ps, we should be able to see.
Nope, oh, we can see it's down. And that's because if I do a Docker ps dash a, and we can see
that this one right here, looks like it stopped 43 seconds ago.
And so it crashed for whatever reason. And that's okay. What we can do is I think we can
actually
see the logs after it goes down. So if I do Docker logs, fast API one, we can actually take a
look
at the logs at what caused it to go down. And we can see a couple things. But we can see that
we get the pedantic error for our settings object, which is all of our environment variables.
So
obviously, a container is like its own separate machine. So it needs all of the environment
variables passed to it, or then our application is going to crash. So how do we pass an
environment
variable in Docker? Pretty simple, we just pass a environment field. And then here, it's going
to
provide a you need to provide an array of environment variables. So I do dash. And
then
here, we just specify our environment variables. So I'm actually going to copy and paste this
section right here. And so these are all of our environment variables. Keep in mind, if you
don't
want to actually write out all of your environment variables, we can actually use Docker to
point to a dot env file or a environment file on your local machine. And if you want to do
that,
it's pretty simple. Here, we would just specify the env underscore file.
And then you can actually provide multiple files. But in this case, you just provide the path
to
the file. So it's going to be in the current directory. And it's going to be dot env. So you
can do either one or a combination of both. I'm just going to use this format so that we can
see
all of the environment variables. And I'm going to just comment this out for now just for
documentation purposes. And now what we're going to do is we're going to do a Docker compose down just to
tear
everything down. And I'm going to bring it back up and detached. And notice this time, it
didn't
build the image. So why exactly didn't it build the image? Well, Docker compose is very
simple.
All it does is it goes to Docker image, basically just runs a Docker image LS. And it looks
for the
image that it would have created. And there's a very specific format that he uses, which is
going to be the name of the folder or the directory, and then underscore and then the service. So
since
the image was already created previously, it doesn't recreate it. However, if you
obviously
make changes to your to your Docker file, and you want to make changes, then you would have to
do Docker up. And if you look for the flags, there should be a dash dash. Let's see, build, this
is
going to force it to build a new image. So keep in mind, anytime the image has to change, and
you've
already but the image already exists, then you have to pass in that flag to force Docker
compose to
let it know that hey, I made some changes, I need you to rebuild it for me because Docker
compose can't detect that. Alright, so now if we do a Docker ps, we can see that it's running now
and
it didn't crash. And we should be able to access it on port 8000. So if I actually go
here,
and just do localhost 8000 with the get request. Looks like it's working. Okay, so now that we
can
actually access our API, which is running in a container, the next thing that I want to do is
I want to set up our Postgres database. And you might be thinking, well, we already have one
installed
on our local machine, why are we setting up another one? Well, a couple things. First of all,
if we're already Docker rising our environment, it makes sense to set up our Postgres
database,
especially from a development perspective within a Docker container, because it's so much
easier to set up a database in a container than it is to install it on our local machine, because we
just
spin up a pre built image. And that's it. So even if you don't actually want to Dockerize your
application, from a development perspective, I strongly recommend you make use of Docker
containers
so that you can spin up a quick database just for testing purposes. But since we already have
a Docker based application, now, we're going to just use a Postgres database that's running in a
Docker
container instead of on our local machine. Because if we take a look at the environment
variables
that we pass to our container, we provide a value of localhost. And so localhost means that
the
container is going to or the API running in the container is going to try to access a
database
running locally on that container, which it's not. So let's create a brand new service, which
once
again is nothing more than another container for running our Postgres database. And so I'm
going
to go down here. And we're going to create another service called Postgres. And here, we're
going to
provide a image flag, right? Because here we did build because we're building a custom image,
here, we're going to use the pre built default Postgres image. So if we go back to Docker
Hub
and search for Postgres, you'll see this is the official Postgres image.
And it is going to contain all of the documentation for setting up Postgres. But the main
thing is there's a couple of environment variables that we have to pass.
So first of all, the password that's going to be used for our database, we can give an
optional Postgres user, but it's going to default to Postgres if we don't provide
it. And then we also want to give it a Postgres database name. So we'll call it fast API, like
we've been using in our development environment. And that should be all that
we have to do for the most part. So here, I'll say environment.
And we'll do Postgres underscore password equals and then we can give this anything we want.
So
password 123. And we want Postgres underscore DB. So this is going to be fast API.
Alright,
the next thing that we want to do is when it comes to data within a container, that data
doesn't persist as soon as the container goes down. Alright, so when you delete the
container,
you lose all of your data. Alright, and so from a database perspective, we don't actually want
to lose our data, we want that to persist, even if we kill off the container. So how do we
actually
save that data, we have to create something that's called a volume. And that's what allows us
to save
data from a container onto our local machine. So if we kill the container, we can always spin
up a new container and just point it to those files. And there's a couple of different volumes that
we
can use or types of volumes and Docker. There's anonymous volumes named volumes. And I think
bind
mounts is the other one, we're going to make use of named volumes. And so to create a named
volume
here under the Postgres database, section or service, we're going to say
volumes.
And then we say dash, and then provide a name for the volume. So you call it whatever you
want, I'm going to call this Postgres dash DB. Right, and then we have to specify the path in the
container
that we want to actually save. So what directory do we want to persist on our local machine?
And the path that the Postgres container or image actually stores all of its data, we can
actually
take a look at the documentation to figure this out. If you search for volume. And so right
here,
this gives us an example. And this is going to tell us what we need to know. So we want to
save slash bar slash lib Postgres ql slash data. So that's where it's going to save all that
data.
So we'll copy that. And once again, this is the path in the container that Postgres is going
to
write to. So that's where all of our tables, all of the entries are going to get stored. So we
want to copy that essentially, and save it on our local machine. And the last thing that we want to do
is
anytime you use named volumes, we have to create another volume section globally, and then
just
pass the name that we created. So whatever is here, you just put it down here. And the reason
we
have to do this is because named volumes are designed so that technically multiple containers
can access them. So you define it down here first, technically, and then multiple containers
can
reference them by just calling that. Alright, so we've got this setup. And actually, I
realized I
left my containers running. So what I'm going to do is I'm going to comment this out for a
second,
save this, and I'm going to do a Docker compose down. That's going to delete everything.
Then
we'll uncomment our Postgres service. Then the next thing that we want to do is we have to
figure
out, you know, what is the host name or what's the IP address of our Postgres database.
And,
you know, there's a couple of different ways to do that. But what's awesome about Docker is
when you use Docker compose, it creates a custom network. And a Docker network uses DNS. So we
can
actually say to reach the Postgres database, instead of like looking for the IP address of
that container, we can use DNS. And I can just say, if I try to reach the name Postgres,
right,
it's automatically going to resolve to our Postgres service. So whatever our service is
called, we can just reference that. And this is going to allow this container to know that when you
want
to talk to the Postgres database, this name here is going to resolve to the IP address of this
container. And so networking with Docker within Docker is so simple, because that's all we
have
to do. And then the rest of the things, the default port, the password, the database name, the
database username, those should all be okay, we can just leave as default. And now I can
then
start this up. And it looks like image something's wrong with the image, I forgot to give the
image.
So what's the image name? It's just Postgres. So that's just whatever this name is, that's all
you
need. Alright, and so now we can see that it started up both. Let's just quickly test our
API,
so we didn't break anything. So that works. And then let's go to create user, make sure it's
point to port 8000. In this case, and if we create user, right, you can see that we have
successfully
created the user. So it looks like we can now access the the database from our fast API
container.
Now, what I'm going to do now is delete everything again. So Docker compose down deletes
everything.
And you can see, I'm essentially able to destroy both of my containers with a single command.
And that's kind of the power behind Docker compose. And ideally, we want our Postgres container
to
start up first, because our API is dependent on the container. So for our API, we can pass
in
another option. And I can say depends on and here we can specify a list of services, in this
case,
just Postgres. And so this is going to tell Docker, I want you to start the Postgres container
before the API container starts. Now, technically, it's not going to wait for Postgres to
actually
fully initialize, it'll just start the container. So you still want to put in checks in your
code to make sure that, you know, if you can't access the database, because it's not
initialized,
you want to keep retrying. And so once again, we can bring this back up with a single
command.
And if you take a look at the order, you can see that we started up the database first. So now
you'll always start up your database first, and then start up fast API. And then you'll
stop
fast API first before stopping the database, because we set up the depends on. When it
comes
to working with Docker containers, we do experience a few extra challenges, especially from my
development perspective. And I'm going to show you how those challenges actually work,
or what those challenges actually are. And if I actually go back to my, my test request,
wherever
that is, where I'm just going to reach the default URL. So if I hit send, you can see it says
Hello,
world pushing out to Ubuntu, right? And that's because in my main.py file is just says Hello,
world pushing out to Ubuntu. Now, if I save this, or change this, and then save this, and then
I
send a request, right, we still get the same response, I don't get the the new text that I
added.
So why is that? Well, there's a couple things that are happening. But the easiest way to
figure this out is to actually poke around in your container and figure out what's happening.
So
I'm going to do Docker ps. And I want to see what the file system of fast API container
actually
looks like. So to do that, we do Docker exec dash it. So that's going to enter into
interactive mode,
we specify the container name, and then we type in the word bash. So what we're doing is
we're
entering interactive mode, and we're going to override the default command of the container.
So the default command is specified right here from the Docker file. So we don't want to run
that
we just want to run bash, which is going to allow us to access the file system. That's the
image
name. So the name of the container is fast API underscore one. So let me add the one. So now
you
can see that we're we get a Linux prompt. So we're in the Linux file system. And if I go into
app,
and then go into, if I do cat, which is going to print out the contents of a file, we'll take
a look at the main dot p y file. And you can see that it just says hello world pushing out to
a
bun to. So when I made these changes, and added these extra characters, it didn't get pushed
out
to our container. And if you're trying to figure out why that is, it's pretty obvious,
right?
Because what we did was we created a image which copied all of our code here, at that point
in
time. And then when we did a Docker compose up, it created the image. And then that was
it.
Like nothing else changes, right? Afterwards, we went in and changed the code, but the image
was
already created. So whatever is in the image, nothing changes after that point. So any changes
we make will not get copied over to our container. So what do we do in this case? How do we
get
around this limitation? Well, what we can do is we can make use of a special volume called
bind
amount. And a bind mount is a special volume because it allows us to sync a folder on our
local machine with a folder in our container. So this app folder, which has all of our
application
code, what we want to do is we want to sync this folder in our container with this folder here
so that if any changes get made here, it's going to get copied over to our container. And so
under
volumes in our API, we're going to define a volume here. And I'll say volumes. All
right,
just like we did with Postgres, very similar, except we're going to use a different syntax,
because this is a bind mount. So a bind mount, we have to first provide the path of the
folder
that we want to sync on our local machine. So this is our current directory. So we can just do
dot slash for current directory. But if you wanted to sync just one individual folder, maybe like
the
app folder, then you can do dot slash app up to you, but I'm going to sync everything. And
then we have to specify the path in our Docker container, which is going to be this folder right
here.
And that's going to automatically sync the the two folders. And if you wanted to add some
extra
security, you can do ro this means read only. So what this does is this makes it so your
container
can't change any of these files, because only we should be changing the files, not the
container itself. So this just adds a little bit of extra security if you wanted to, but it's
still
technically optional. And let me exit out of here. And now we'll do a Docker dash compose
down.
And we'll do a Docker compose up again. And now let's just change this together altogether.
So
bind mount works. So if we see that, that means everything's working. So let's save
it.
And if I send, we can see that I get the old one now. Right. So whatever I changed here
doesn't
get copied over. So it looks like we didn't fix the issue. So let's actually take a look and
see
what's happening. And so how do we do that? We'll do the Docker exec command again, to go into
the file system. Let's go into the app folder. And let's cat that main.py file. And this is
interesting
because it looks like the bind mount did work, because we can see that the code got updated.
And
if I make another change, again, this time, let's just add sort of. And if I cat this again,
we can
see that it's in there. So the bind mount is working. And it's syncing the code with our local
directory
and our container. But there's one issue. And if I go back to my Docker file, you can see that
I have
my command right here. But this doesn't have the dash dash reload flag, which is what we need
to restart the application. So anytime we make changes, our code, we need to automatically
reload
when it detects any changes so that we can actually see the new application. And we didn't
provide that in the Docker file. And so that's why it's not working. But this is actually a pretty
simple
fix. We just add the dash dash reload flag. So let's tear everything down. And we have a
couple
options. So I can go here and say dash dash reload. And that would work just fine. But instead
of
doing that, what I would rather do is I like this default one, because this is the one you
technically use in production, because you don't want you don't want dash dash reload in production. But
in
our Docker compose file, we can actually override it. So we can provide another flag called
command.
And here we can provide a command to override that from the Docker file. So I'll say,
Docker,
sorry, uvcorn app dot main app dash dash host 000 dash dash port 8000 dash dash
reload.
Alright, and let's bring this back up. Let's just see what it is currently right now. So we
got whatever it is now. And let's make a
quick change. And I'll just delete all of this and change this back to Hello
World.
And let's test this out. And we can see that it does successfully update. And so now
our
bind mount works, and we automatically restart our code. So our development environment more
closely
matches up with what was what we had before, before we move to Docker. Okay, so if you
haven't
already done so, go ahead and create an account on Docker Hub, it's a free account. But I want
to
show you guys how we can set up a repository on Docker Hub, so that we can actually store our
images here. And so Docker Hub is kind of like the, the Docker equivalent of GitHub, where
it's
kind of acting as a repository. But instead of a repository for our code, it's a repository
for our
images. So we can upload our images, anytime we make a new image, we can actually upload it so
that we can track changes to our images over time. So make an account, and then we want to go
into
repositories. And we want to select create repository. And then give this a name, it
doesn't
really matter what you want to call it, I'm just gonna call it fast API. And then we're gonna
do public because you only get one private repository. And so now we have a private repository,
well,
it's the public repository. And so this is the URL to the repository, which is going to be
your username, this is my username. And then this is the name that I gave it to fast API. And so
now
what I want to do is I have an image, right, if I do Docker image, LS, we could see that I
have my
fast API underscore API. This is going to be the image that I want. And so how do I actually
upload
this image to to Docker Hub? Well, there's a command called Docker push. Alright, so if I
do
a Docker push dash dash help, you just pass in Docker push, and then the name of the image.
So
let's try doing that. We'll do Docker push. And then the name of the image, which remember
yours
is going to be a little bit different, because it's going to be based off of your, your
folder. Let's try this. And it says requested access to resource is denied. So the first thing that
we
have to do is we actually have to do a Docker login. So we have to log into our account. So
you do Docker login here, it's gonna ask for your credentials. So we'll do sloppy networks, in
this
case for me, and then it's gonna be whatever it is for you guys. And then now let's try to
push it
again. And once again, we get a access to resource is denied. So what exactly is happening
here?
Well, when we want to push up a image, right, there's a very specific naming convention that
we have to use, right, we have to name it exactly like this sloppy networks slash fast
API.
And we even see an example right here. So you do Docker push, the name of your email, or your
account name, then the name of the specific repository. And then you can give it an
optional
tag name. So what we got to do is we got to rename our image. So how do we do that? I think
it's
Docker image, I think, well, let's do dash dash help, just to see if we can figure it out. We
want
tag. So this essentially copies an image and renames it for you.
So let's do Docker image tag dash dash help.
And here, we just provide the the source image and then the target image. So what is my
image
called again, it's gonna be this one right here. And we just do Docker image tag, then the
image
that we want to rename, and then we give it the new name, which has to be your account,
slash,
then the repository, and then you have an optional, you know, tag, the default is just going
to be
latest. So I'm just going to leave it without a tag. So I do that. And if I do a Docker image,
LS, we should see the new one right here. And so now I can do a Docker push.
It looks like I didn't copy that. So let's try this one more time.
And now it looks like it's pushing something. So let's give this a minute or two. And then
once it's done, let's take a look to see if we have successfully pushed it to our repository.
All
right, so it looks like it's successfully pushed. So now if I go back to my Docker Hub page
and do a refresh, we can see that last pushed was a few seconds ago. So now we've successfully pushed
it
to our Docker Hub. And so you know, if we set up an actual production Docker environment, we
could
just pull the image from this specific URL or this specific repository. Now, before we wrap up
the
Docker section of this course, there's one last thing that I want to show you guys. And that
is
that if you take a look at what we have right now, we have a identical clone of our previous
development environment, but we're now developing within a Docker container. So our Docker
container
environment is a exact clone of our previous development environment. The issue is is
that,
you know, when we move to production, assuming that you're using Docker in production, which
is kind of the main idea behind Docker is that your development environment and your
production
environment are almost exactly the same because you're using the same exact Docker images and
containers. But if we try to use this Docker compose file in production, there's going to
be
a whole bunch of issues. First of all, obviously, these environment variables would need to
change and we can't hardcode them, we wouldn't want the dash dash reload flag. And you certainly
don't
want to bind mount because the code shouldn't be changing. And then the ports that we use
could also potentially be different. So even though Docker or so even though the Docker
environments
from our production development environment should be roughly similar, there are going to be
some differences. And so we need to account for that. And so what I like to do is I actually like
to
create two different Docker compose files, one for development, and one for production. So
that's what we're going to do. But first of all, let's do a Docker compose dash down that we can just
tear
everything down for a second. And once that's down, I'm going to copy this and paste it.
And
we're going to rename these files. So this one is going to be called Docker compose dash dev.
So
this is going to represent our development environment. And this one is going to represent our
prod environment. Alright, so we've got our dev and our prod environment, the dev doesn't
need
to change because we had already set that up for development environment. The production one
is going to change a little bit. So first of all, the command, right, we don't want the dash dash
reload,
so we could specify the exact command we want, which is the one without the dash dash
reload.
And that should be all that we need. However, keep in mind, this is the default command from
the Docker file, right? So we could technically just remove this, or just comment it
out.
And so I'm just going to comment it out in this case. But if you wanted a specific custom
command for production, you can just provide that here, the whole idea is that this file is going to
represent
all the changes for our production environment. Then maybe we want instead of listening on
port
8000, we want port 80. So that we can use our web browser. Most of the things can stay the
same,
the environment variables definitely need to change. And so with a production
environment,
we first of all, we're not going to hard code the values, we usually want to grab these, these
values from the environment variable set on the host machine. So instead of actually
hard coding these in, instead, we can reference environment variables. So to reference an
environment variable, what we do is we do dollar curly braces, and then the name of the
environment
variable. So it's going to look on the you know, Linux machine that it's running on for a
environment
variable called database host name. So I figured we would just have the match, just to show
you what that would look like. And then we're going to do the same for all of these. And I'm just going
to
copy and paste this once again. So it's going to look like this. So it's just grabbing the
data,
the environment variable from the local machine, keep in mind, they don't technically need to
match. But it just makes it easier for troubleshooting purposes.
And then for the Postgres database, right, we want the password to be the same environment
variable.
And then we would add the database name so that we're not hard coding that one as
well.
And we don't want the bind mount. So we will remove that.
And that's about it. So that's all you have to do. So this just gives us both a development
environment and a production environment. We'll save all of that. Now starting up Docker
compose
is going to be a little bit different now because we don't use the default Docker dash compose
dot yaml file name. So if I do a Docker compose, if I can find that the up Docker dash compose
up,
you're gonna see that it throws an error, right? Because it says it can't find a Docker
compose dot yaml, or dot yaml with an A, or compose a yaml or compose a yaml with an A. So it
looks
for only these file names. So when we have a custom file name, we have to do Docker dash
compose. And then we pass in the dash f flag for the file name. And so we provide the path to the file.
So
it's just in the current directory. So I could just do Docker dash compose dash dev dot
yaml.
And then we just do an up and then dash D. And then we can start it back
up.
And if you want to bring up a production environment, then you would do dash dash prod. That's
the only difference. So you don't really have to do anything else
other than running the different specific Docker compose file. And the last thing that I want
to
change is actually, in your production environment, you don't actually ever want to build the
image in a production environment. Instead, what's recommended is that when you're done
developing,
you push the brand new image to Docker hub, right? And then in the production
environment,
we just pull this image from Docker hub. So how do we pull this image from Docker hub? Easy,
we chain we remove this build option. Remember, this is going to be in the prod file.
And we set this to be a image. And here, we just set this to the name of the repository. So we
just
do, you know, your username slash fast API dash, not dash app, just fast API. So whatever
is
essentially right here. And then if you want a specific image with a specific tag, then you
would add the tag as well. And so that's all we have to do, feel free to stop your containers now if
you
want. And even when you do a down, you want to make sure you do a dash F. So it knows which
file to
actually look for. And I misspelled that. And that's going to conclude the Docker section
of
this course. And so I think this is going to give you a solid foundation on how to Dockerize
your fast API. Now currently, whenever we make changes to our code, whether it's implementing a
new
feature or modifying the behavior of a previous feature, or even fixing some bugs that we
happen
to catch, right, we have no idea if the changes we make break other functionality in our
code.
And so normally, anytime we make any changes, we then have to, you know, go into postman, and
essentially manually test out every single scenario to ensure that things are working.
And this isn't a very scalable solution. It's going to result in a lot of wasted hours for
every single time you have make any changes to your code, you're gonna have to test
everything
because you don't know what, what code you've broken by making those changes. And so
this
is where, you know, an automated testing library comes into play, where we can define a
whole
bunch of tests to test a majority of the functionality of our code. And so that way, anytime
we make changes to our code, we can run our test to see if we've broken anything.
And keep in mind, you know, obviously, we can't test for everything. But we can at least test
for enough things that we can catch some of the more obvious bugs. And so this section of
the
course is going to be dedicated to getting started with testing and really understanding the
concepts of testing, and seeing how we can get started with a library like pytest.
But I do want to provide one little disclaimer before moving forward. I wasn't originally
going to post a section on testing for this course. And the reason for that
is that I don't have much experience with testing, it's not something I do a lot of or in, or
at all, really. And so I want to make sure that you guys understand where I'm coming
from
when it comes to testing. And that is that, you know, since I am not super proficient at
testing,
you know, I don't really feel too qualified to actually teach it. And so what I want you
guys
to do to take what I want you guys to take out of this section of the course is just to how to
get started with testing, how do you set things up? And what's the main ideas behind a
test,
I may not necessarily be teaching the best, most efficient way to test, I may not be using the
most
efficient way of testing. And there may be scenarios where I may not even be following best
practices. But I want to show you guys how we can at least get some integration tests set up. And then
at
that point, what you guys can do is you can just forget everything else that I've taught you
when it comes to testing. Once you at least have the groundwork, then you guys can, you know, do
your
own research, figure out how you ultimately want to implement testing with any of your
projects,
and then go ahead and just run with that. So with that in mind, in the next
video,
we'll get started with get getting pytest installed and writing our first test. For
testing,
the library that we're going to be using is pytest. So if you do a quick Google search, you
can just search for pytest. And the first result is going to take you to the
documentation
page for the pytest library. And so I recommend you guys take a quick look at this just to see
how it gets set up. And if you want to do a little bit of reading before we actually get
started
writing our first test, definitely go ahead and do that. But once we've got that opened
up,
what we can do is we can install pytest. So we can just do pip install pytest, just like we
would with any other package. Then once that's done, we have access to the pytest command. So just
go
ahead and run pytest and see what happens. So print out a few things, you notice that we can
see that the test session starts. So this is when we start testing, I print out some
information
about the platform that we're running on. And that's okay, we can ignore all of that. And
you'll notice that it says collected zero items, we'll talk about this in a bit. But the more
important
thing to look at is it says no test ran in point zero four seconds. And this is expected, you
know,
we haven't created any tests. So obviously, it's not going to be able to run any tests. But to
run our testing, all we have to do is just run this command. And then you'll see that
pytest
will go through all of your tests run it to verify, you know, that they all pass. And if
there's any errors or anything like that, it's going to report that. So it really is as simple as that. But
let's
go ahead and create our test. And I want to store our all of our tests inside a directory
called
tests. So inside the root directory of your project, just create a new folder, we'll call it
tests.
And technically, you can put this folder anywhere. If you wanted to put this test directory
within your app directory, you absolutely could. It's a matter of personal preference, I'm just going
to
keep it outside of the app directory for now. And within here, I'm going to actually create a
file.
And this is going to store our first test, I'm just gonna call this my test dot py for now.
All
right. And we obviously need to test a part of our code, whether that's a a route or a path
operation
or a function, or a class, we need to test something. And right now, our code is a
little
advanced. And when I say advanced, I don't mean advanced, because anything we've done is super
advanced. So far, it's advanced from the perspective of testing, where we're coming in from a
background
of no knowledge of testing. So we want to start out with testing the simplest functions. So
I'm going to actually create a brand new module and essentially create the simplest module or
function
that I can possibly think of. So I'm going to create a new file, call it whatever you want.
I'm going to call this calculations, because it's essentially going to be a bunch of functions
that
we use to perform certain calculations. And I'm going to define the world's simplest
function,
which is called add. And I think you can guess what this function is going to do, it's going
to take two numbers, which is going to be type int. And it's just going to return the sum of
them.
So we just say return num one, plus num two. Right. And the reason once again is because
this
is going to be a very simple when it comes to testing. So you guys can at least have an
understanding of how to actually perform these tests. So we've got this module defined
inside
our app directory. And then in our test folder, we've got our my test p y. And we're going
to
define our first test. So what is a test? Well, it's really just a function. It's either a
function
or a method within a class, we're going to start off with just doing functions, because it's
just a lot easier. And we're going to call this function test underscore add. Why did I give it that
name?
Well, it's testing the add function. So I think that makes sense. And technically, the name,
the naming does matter. But we'll come back to that in a bit. Now, how does this function
actually
work from a testing perspective? Well, it's very simple. You know, pytest will run through all
of
our tests. And when it runs through this specific test, what it's going to do is if it runs
through
it, and there's no errors get thrown. That means the test has passed, it's succeeded, that
means
our code is good. If our function throws an error of any kind, it's going to consider our
function
or our test as failing than the test has failed. So how exactly does that work? It's a
simple,
it's simple, we use something called assert. And if you aren't familiar with what the assert
command
does in Python, anytime you assert a true value, nothing's going to happen, the code just
runs.
However, if you assert a false value, that means it's going to throw an error. So it's, it's
no
different than raising an error or throwing an error. In Python, it's just a simple way of
doing that for testing purposes. If we return something that's true, then the assert won't do
anything.
If we return something that's false, then it will raise a Python error. And I'm going to show
you exactly how that works. So if I just say assert true. And before this, I'm just going to do a
print
testing add function. And then down here, I'm going to just call the test underscore
add
function right here. And I'm going to just run this as a normal Python module. So we're
not
going to even treat it as an actual test or anything like that. We're not even going to use
pytest. So I'm just going to call Python. And keep in mind the commands Python on on
Mac,
but it's pi dash three on Windows. And then we're going to go into the test
directory.
I want to call my my test.py. And I'm going to show you what happens. All right, we can see
it
says testing add function, which is from this print statement. And then you can see the assert
did nothing. So it just ran and everything's fine. And that's expected because like I
said,
assert only does something when we return a false value. So if I say false now, and I run this
code,
take a look at what happens. Hey, look, Python throws an error. So assert is simple as
that
true means nothing's going to happen false means it's going to throw an error. And we don't
have to just say true or false, we can actually pass in a condition. So if I say one equals equals
one,
that's going to return true, right? So technically, an assertive true won't do anything.
However,
if I say one equals equals two, that's going to be a false statement. And if I run
that,
we could see that it asserts and it throws an error. So that's how we make use of
assert
within our testing functions. So I'm going to remove this. And we're actually going to test
out
our code now. So to test out our add function, the first thing that we have to do is we
actually have to import it into this file. So we're going to import it. So we'll say from and we have to
go
into the app directory, and the calculations model module, so we'll say app dot calculations,
import
add. And now what we're gonna do is we're just going to call the add function like we normally
do. And we need to give it some test data. So I want you to think of two numbers that you
want
to use for testing, add them up and what is the result should it be. So I'm going to say I'm
going
to do five and three. And I know that this function if it works properly should return a value
of eight
because five plus three equals eight. And so I can just take the sum, store that in a variable
called
sum. And what we can do is since I know some should ultimately be equal equal to
eight,
I can assert that and that should there should be no either. So I can say sum equals equals
eight.
So that means if if our code runs fine, some should ultimately equal eight. And if we assert
eight equals eight, that means it's going to return true, and nothing should happen. Or
pi
test should consider it as passing. However, if I mess up my code, and for some reason to
add
function returns a value of 20, then when we do assert 20 equals eight, well, that's going to
be
false. And it's going to throw an error. And it's going to consider that as a failing test,
because an error means a failed test. So that is the main idea behind a simple automated test within pi
test,
right? You just provide you run the code with some test data. And you know what the result
should be,
because you created this test data. And you just make sure that the result of the code matches
with your expected result. So let's actually run our test now. So I'm going to do
pi test. And let's see what happens. Right. And you see the same exact thing we saw before.
Once again, it says it collected zero items, no tests ran. And so why exactly is this happening?
Well,
if we go to our documentation, and you scroll down to this feature section, you go to the
auto
discovery section. And so this auto discovery section is going to explain how pi tests finds
all
of the tests in your code. Because it does have a auto discovery functionality where it can
actually go through all of your code, all of the directories and packages and recursively look through
them
and find the tests. But it looks for specific keywords. And you can see in these
directories,
it looks for any files that start with test. So you have to do test underscore and then some
name,
or some name underscore test p y. And if we go to our code, right, we named this my
test,
this does not match what it's going to look for. And that's why it doesn't actually find any
test from that. So let's change this, I'm going to call this, I'm gonna rename this and I'm going to
call
this how about test underscore calculations. Alright, and so now, if I run this, you can
see
that it throws an error. But I realized that, you know, within my tests package, I do need to
have
the usual underscore in it underscore underscore dot p y to actually make it a package. So
I'm
going to do the same thing, I'll do underscore underscore in it, underscore underscore dot p
y,
because of that, it wasn't able to actually import the file properly. So let's let's save
that. And
let's try this again. So now if I run this, you could see that, hey, look, we collected one
item.
So I think it's safe to say that this collected one item probably means we have one test. So
the
number of tests equals items. And then we could see that inside the test package, we have a
module
called test underscore calculations dot p y. And then there's a little dot. So the dot
reference
represents one individual test. And a green dot means good. A red dot means it failed,
right,
green is passed, red is fail. And so if you have two tests, then you would see two dots. And
then if the second test pass, it would be green, if it failed, it would be red, simple as that. And
so
now we have successfully ran our first test. And just to make our code a little bit
simpler,
right, I could just instead of having the sum variable, I can just cut this out, just paste
it
into here just to make it a little bit cleaner. And then we can remove all of this. And just
to
make sure I didn't break anything, let's run our test again. And we can see that once again,
it worked just fine. Now, when it comes to the auto discovery of files, a few things to
note,
you don't necessarily have to follow this convention of test underscore calculations, if I
actually rename this, and I call this, you know, my underscore, what did I call this my
test
dot p y. Right now, if I obviously run this, it's not going to find any tests, but I can pass
in the
specific path, or the specific module that I want to test. So if I do my test, dot p y, then
it is
able to find the test. But I like to have pytest be able to automatically find all my tests so
that
I don't have to specify this. So we'll rename this and go back to test underscore
calculations,
because it ultimately is a module that's going to be testing my calculations
module.
And another thing to note the name of the test within the module also matters. If I call
this,
you know, maybe something like testing underscore add, take a look at what happens. Whoops, I
wanted to auto find it now. And I forgot to save it. Now, if I run this, looks like it
still
works. But let me change this to just something random. Right. And so now we can see that no
tests
ran. So the naming of the function absolutely matters when it comes to auto discovering
tests.
And you want to stick to the standard of test underscore something. And I think somewhere
in
this documentation, it's going to go over here we go test from those files, collect test
items. So
a test prefix test function or method outside of a class, or a test prefix test function or
method
inside a test prefix test clause. So it's got to start with test essentially, it doesn't
necessarily have to be test underscore. That's why testing underscore add work to just fine. It does
have
to start with the word test. That's why if I said add underscore test, I don't think this will
work actually shouldn't based off of their description, and it doesn't. So keep in mind that the
naming
does matter. And I recommend you stick to the convention of naming all of your testing modules
test underscore, and the name of the module that you want to test, and then your functions
test
underscore and then the name of the function that you want to test or the functionality that
you want to test. Now a couple of things to note, I'm going to run this test again. And you'll see
that
it doesn't really give us much detail as to what's happening. But we can pass in other flags
to get
a little bit more detail or to change the functionality. So if I do pytest dash dash help,
you'll see that there's a lot of options. But I think something that would probably be
helpful
is like a dash v for verbose. And I think we should have that in here. So let's see if I can
find that here we go dash v is increased verbosity. So we'll just do pytest dash v. And let's see
what
happens. And so now instead of just having a dot, it's actually going to list out the specific
test
that's running. So I think that's a little bit more helpful. And I usually like to run these
tests with a little bit of extra verbosity, just that I know what's happening. However, maybe in
an
automated environment, you don't really care about that. And the other thing to note is that
we have this print statement here. But nowhere in the output, do we actually see anything get
printed
out? I don't see testing add function anywhere in here. And the reason for that is that by
default, pytest will not actually print out any of your print statements, we have to pass in an
extra
flag to do that. So to see the print statements, you have to pass in dash s. So now if I run
this,
we can see that we ran this specific test, and it ran testing add function. And so keep in
mind,
anytime you want to print something, you got to pass in the dash s flag. Okay, guys, so
testing
really is as simple as that. But before we go on to more complicated tests, I want to just
make
sure that we fully understand how this works. So we're going to create a few more dummy
functions
that we can use for testings. And if I go back to my calculations at p y, I've already written
this out. So I'm just going to paste it in here not to waste your time. But you'll see that they
all
essentially do the same exact thing, they just perform some sort of mathematical operation. So
I've got the subtract function, which just subtracts two numbers, I've got the
multiply,
which adds two numbers. And I've got the divide, which just divides two numbers. So let's test
this out. Now, the first thing that we got to do is, well, let's import all of those. So
they're
subtract, multiply and divide. And let's define our function for subtracting. All right. And
so
just like before, I'm just I'm going to do a one liner. So I'm just gonna say assert and we'll
say subtract two numbers. What numbers do I want to subtract? How about nine and four?
Oops,
nine comma four. And we know that nine minus four should equal five. So I can do assert equals
equals
five. All right. And if I run this again, all right, we can see that both tests
pass.
And just to make sure that you know, our testing actually works, let's actually go into our
code. And let's essentially create a bug. So if I want to add two numbers, right, we know that's
gonna
be num one plus num two. But let's say, you know, one of our moron colleagues went in
and
added one, which is ultimately going to cause our function to break. And so ideally, if
everything
works fine in our testing, it should be able to catch this. Because if we add five plus three,
it's going to do five plus three, and then it's going to add that extra one, it's going to
return
nine, nine equals equals eight is going to assert to a false, which is going to throw an
error. So if we try this out now, we can see that it did in fact fail. And because we set this to be a
verbose,
we can see exactly what happened. So we ran the test add. And what happened was it is asserted
add five comma three equals equals eight. So it's asserting nine equals eight. And
that's
going to throw an error. And so that way we see one failed one pass of the subtract function
still
passed, because we didn't touch the code for that. So I'm going to go back and fix our bug in
this
case. And we're going to just set up a couple more test functions for multiplying
divide.
And I can just really just copy these actually. I'll just change this to
multiply,
divide. And we'll just select a couple of numbers. So let's say
and I realized, looks like that's a typo, this should be multiply with an
L.
All right, and we'll say four and three. So when we multiply four times
three,
that should equal 12. And for divide, we'll change the function that we're calling to be
divide.
And we'll do 25. So 20 divided by five should equal four. And so now if I save
everything,
we should ideally see all four of our tests pass. Let's try this out again. And you don't have
to
use the dash v or the dash s like if you're not printing anything, or you don't want to see
it, you don't need the dash s. And maybe you don't want the verbosity. So you could
technically
remove those as well. And we can see that we get four dots, which means all four of our test
functions have succeeded. If you're like me, the first thing that you may have noticed is
that,
you know, testing with one set of numbers doesn't seem ultra reliable, right, that doesn't
necessarily
assure us that the function that we wrote actually does what it's supposed to do, I would
ideally like to test with a couple of different sets of numbers. And the way to do that is well, you
know,
I'm sure if you just took a guess, right, you're probably thinking, well, maybe we just copy
this function, paste it, whoop, that should be back here. And then maybe just call this test add
to
maybe we don't need this print statement anymore. And then you provide the other sets of
numbers. So maybe like four, and two should equal six. And then you try this out. And you run
this.
And we can see that that worked. And technically, you can do that. But pytest provides us a
simpler
way of doing this. So I'm going to delete this, because we don't need that. I'm going to show
you how we can do this using something called parameterize. So I'm going to import pytest. So I'll say
from
I shall just say import pytest. And we're going to add a special decorator from the pytest
library.
So we say at pytest dot mark dot parameterize. And so here, we can essentially provide a
list
of numbers that we want to use for testing, and then provide what is the expected result. And
so you know, our function takes two inputs, five and three. And we also need to provide
what
is the result. So these are all the values that we're providing, we have three values for this
specific function. So we're going to tell this specific decorator, what are the three things
that
we want to provide for each test case. So in quotations, we'll do num one, num two, and
then
the expected value. Now what you call these doesn't matter, it's up to you, right? But these
are going
to be treated like variables, essentially. So you can call them what you whatever you want,
you can call this x, you can call this y, you could call this result. It doesn't really matter. But
I'm
going to stick with num one, num two and expected. And more importantly, take a look at how
this is
set up, right? Your instinct is probably to do the end quote here and then quote here, quote
here,
and then quote here like that. That's not what we want. That is not how it actually works,
which I don't know why they decided to do like this, but that's just what they've
chosen.
So instead, you remove these. And everything should just be one big string. So that's
the
first argument into this decorator. The second thing is going to be a list. And it's going
to
be a list of tuples and each tuple should represent one specific test case. So the first
tuple,
we're going to provide our test case values. So we want what is the value of num one. So in
the first test, let's say we want three for num one, what's the second one, that's going to be
num
two, so it's going to line up with what we've put here. So say 32. And then what is going to
be the
expected value? Well, three plus two should equal five. So we just say five. And so that's our
first
test. If we want to add a second test, just do comma add another tuple. And let's just add
another set of numbers. So let's say num one is seven. num two is one, we know that should equal
eight.
And just one more test. And here, I'll say 12. And four should give us 16. All right. And so
now
what we can do is we can pass these variable names into our test underscore add
function.
So I'll say num one, num two, and expected. Alright, so you want to make sure these match
up
with what you've placed here. Because if this was called x, then you want to make sure that
this is called x. But we'll change that back. And now in our code, we want to replace all of our
test
values with these variables. So this is going to be num one, this is going to be num two. And
this
is going to be expected. And so now if I run this, and we'll just do a dash B just to see in
more in
detail what's happening, take a look at what's happening. Now, we can see that test add ran,
and it ran with 325, which is the first test case, it passed, then it ran the same exact test
with
these values 718 that passed, and then it ran with these values. And that ran. And it passed
just
fine. And then finally, all of the other functions also passed as well. So this is one way of
running
the same test over with multiple values without having to just define 345 functions and
just
copying and pasting. It's a little bit cleaner doing it like this. So till now, we've just
tested
some simple functions. And you'll see that when it comes to testing, anytime you want to test
a function, it's actually pretty easy. However, when you're working with classes, it gets a little
bit
trickier. And it usually involves having to write a little bit more code. So I've created a
simple dummy class in this case called bank account. And this represents a person's bank account. And
I've
already written this out because I didn't want to waste your time writing this out. So if you
want to pause this video and just copy down the code so you can follow along, go ahead and do
that.
But let me just quickly walk you through what this specific class is doing. But like I said,
this represents a bank account. So first of all, we've got the constructor. So
when we create a bank account, we can create a bank account with a specific amount of
money,
which is going to be referenced by the starting balance value. And then it's just going to set
the balance property of the bank account class to be whatever you provide. If you don't
provide
a starting value, it's going to default to a value of zero. Then we've got three different
methods,
we've got deposit, which means we're going to deposit money into our bank account. So all
we're doing is we're going to pass in a value. And then we're just going to add it to our total
balance,
withdraw is the opposite, we're going to subtract a certain amount from our balance. And then
collect interest is simple, we're just collecting interest. So here, we just take our
balance and then multiply it by 1.1, which I guess would represent a 1% interest rate. And so
those
are the three or four functions a methods and if you want to count the constructor method as
well,
that our bank account class has. So I want to go ahead and test this out now. So we'll go to
our
test calculations. And first of all, we want to import our bank account class. So let's
create
a test for our bank account. And the first thing I want to test is, let's create a brand new
bank
account, where we set the default value, the initial amount to be 50. So here, I'll say
test.
And I'm just gonna call this maybe test bank, set initial amount, I want this to be as
descriptive
as possible. And so here, what we're going to do is first of all, we're going to create a
brand new bank account instance. And since this test is going to test to ensure that we have actually
created a
brand new bank account with a certain amount of money, I'm going to provide a value doesn't
matter what value you decide to use, but I'm going to use 50. And we'll just store this in a variable
called
bank account. All right, and so to test if our function actually worked, or the
functionality
of our constructor method actually worked, what we're going to do is we just say assert. And
we
want to check the bank account dot balance property equals equals 50. Because we set
the
default value to be 50. And if we just quickly go back to our code, you can see that we set
the starting balance, or we set our balance property to be whatever the starting balance is. It
just
happens to be a default of zero if we don't provide one, but we are providing one. And that's
what we're testing. So we can test this, let's run our code now. And let's see if this worked. We can see
that
it did in fact work. And just to introduce a bug, maybe we set starting balance to
be
you know, minus one, right? So we've broken our code. Now, if we run this one more
time,
you can see it failed because we subtracted one. So we're asserting 49 equals equals 50, which
throws an error, which means we ultimately fail the test. Let me remove that bug.
Alright, the next thing that we want to do is let's test to make sure that the default
starting balance works as well. So I'm going to create another function.
And I'll call this about test bank, unscored default amount.
So in this case, we're just going to create a brand new bank account. And we're not going to
pass in a value. So it should default to zero if our code works.
And then we'll store this in a variable just like we did before. And then we'll
assert
I can copy this. But now it should equal zero. Okay, and let's test this out
now.
And it looks like it works. So great. And let's just quickly go ahead and test our last two
functions out. Sorry, we got three more actually. So let's test the withdrawing functionality.
So
we'll do test underscore withdraw. And here, we're going to initialize another bank
account
instance, because we have to do that every time we want to test it. I'm going to give it a
default value of 50. It doesn't have to be 50. Once again, you can choose whatever number you
want.
And we'll store that in a variable called bank account.
And we'll call the bank account withdraw method. And we're going to withdraw, you know, let's
say
$20. So we start out with 50, we withdraw 20, we know that the result should be 30. So if I
just
copy this assert statement, once again, I can just change this to 30. And it should be no
problem.
Run this, once again, all of our tests have passed. Let's try the
deposit
functionality now. So I'll create a test to test that.
And I'm actually just going to copy all of this code, because it's pretty much identical.
You'll see that testing can become a little repetitive, repetitive. We'll start out
with
an initial value of 50 doesn't really matter. We'll change the method to be deposit. Maybe we
want
to deposit $30. So 50 plus 30 should equal 80. We test this out. Looks like everything's
working.
And the last thing that we want to test is the collect interest method. So let's create a
function for that. We'll do test underscore collect underscore interest.
And once again, I'm going to copy this code. And we'll just use it here, which is going to
change the method to interest or collect interest.
Okay, so we start out with the value of 50. And so what should the interest be? Well, if I
take a look
at what my method actually does, it just takes the number multiplies by 1.1. So that should
give us
55, right? 1.1 times 50 should be 55. So I will say this should be 55. And let's see what
happens.
So if I run this, we can see that we did have one test failed. And I'm assuming it's going to
be the
last test, the one we just created. And we can see that the test collect underscore interest
failed.
And the reason for that is that it looks like bank account dot balance returns a value of 55
dot
000001, which in Python is not the same thing as 55. So this isn't necessarily an issue with
our
code. It's just the way that, you know, we're comparing numbers in our test. So this is an
issue with our test in this case, because it's not accounting for some of the different
rounding
and the way the calculations occur within Python. So what we're going to do is what we can
just call the round method right here, and we can round it to, you know, five or six decimal
places,
depending on how accurate you want it to be. So it should now round it to, you know, this
value,
however many decimals is five or six, and then convert it to an integer. So then we can
compare these two because this is an integer, this is technically a float. So not exactly they're
not
going to be equal in that case. So now if I run this, we can see that it did successfully
pass.
So that's just something to keep in mind. That's why I added that little extra test case, just
to make sure that especially when it comes to those calculations, you got to make sure
that
you're comparing the right two numbers. If you look through all of the test cases for the
bank
account, you'll notice that there's some repetitive code, right for every single test case
that we have for testing our bank account, you'll notice that we always have to initialize a bank
account
instance. And you can see that right now, look at that we are repeating our code across all of
our
tests. And when you get to more complex classes, more complex test case scenarios, right, you
might
have 50 tests for a single class. And we're in this case, just repeating the same exact
code
for each one of them. And so when it comes to writing these tests, you know, pytest does
provide us with some tools that we can use to minimize the amount of repetitive code. And
one
of those is called fixtures. And a fixture is a very simple concept. It's nothing more than
a
function that gets run before each one of your tests, or at least the specific tests that
you
want. So we can create a function, which does nothing more than initialize a bank account
and
then return it to our function that's about to run. So I'll show you how we can do that.
Right,
the first thing we have to do is let's create our fixture. And I'm going to we can put it
anywhere, I'm just going to put it here for now. But generally, you want to put it at the top
of
the file, actually, I'll stick to the best practice, I'll say, at pytest dot
fixture.
And then here, we create a name for our fixture. So I'll say, this is going to, you know, give
us
or initialize us a bank account with a zero initial value. So I'll call this zero underscore
bank
account. And so all we're going to do is we're just going to return an instance of bank
account
with an initial default value of zero. And then we'll create one more
fixture.
And this one is going to return a regular bank account with an initial value. And what value
we
want to give it we get to decide. So I'll just say return bank account. And in this case,
50.
All right. And so now what we can do is, for all of these tests, I don't need to initialize
them
here, because I can have these guys reference the fixture. So to assign a fixture or have a
fixture
get called for a function, we just pass it in as an argument. So for test bank account set
initial amount, we're going to reference the zero underscore bank account fixture. So what exactly happens
is
when pytest goes to run this test case, because it sees this as an argument into the
function,
it's going to oops, that should be a zero, okay. But what it's going to do is it's going to
call
this function before it actually runs the test case. So it calls this function. And then
whatever
we return here, which is the bank account gets passed into a variable called zero bank
account.
And so now, instead of creating a bank account instance, I can remove
this.
And I can reference this variable, which is zero underscore bank
account.
But let's test this out. Let's see if that broke anything. If we run this up, sorry, guys, let
me undo this, I didn't mean to use this function. This is going
to be the for the test bank account default amount. So we want to remove this one. My
apologies,
and we don't need that in this one. And then this will be zero underscore bank
account.
But now all tests should pass. Alright, so everything passes. But if you're still a
little
confused as to the you know, what exactly is happening behind the scenes, or what is the order
of operations, I'm going to add a few print statements. So I'm going to say, testing
my
bank account. And then I'm going to put in a print statement inside my fixture as
well,
which is say, creating empty bank account. And so now if we run this, I want you to take a
look at
the code. And I realized I have to pass in the dash s flag to see the print statements. All
right,
so here we go, we're going to test bank set initial amount. So I know test bank default
amount. And
so you'll see that when we start this test, first things first is we create an empty bank
account.
That's that print statement for the fixture. So before the test even runs, we call the
fixture, because we've defined that here, by saying, where is it that it's going to be a argument into
the
default amount test case, then after the fixtures run, which is just a function, we then run
our
actual test case, and then we get the print testing my bank account right after that. So once
again,
just to really drive home this point, a fixture is just a function that runs before a specific
test case. That's all but the great part is we can use the same fixture for all of our other
tests.
So let's go to the next test, which is setting an initial amount. And in this
case,
we have this fixture right here, which is the bank account one. And so I'm going to say this
is going to go to bank account. So this will call the bank account fixture, and then we can
remove
this line. And we can just keep this here, because it's referencing this variable
now.
And this should work now. So we run this once again, all tests pass. And I can just do
this
for all of them. So I can just call the bank account fixture. And I can just remove
that
line. Everything else can stay the same. And the same thing here as well. And finally, for the
last
one, we can do that. And so now you can see that we have saved us ourselves a couple of lines
of
code for each function. And if I run this, you can see that all the tests pass. So at this
point,
you're probably thinking, you know, this does this doesn't really look like it saved us a lot
of code or a lot of, you know, coding out, however, there's going to be certain things that you want to
do,
that you want to initialize before every single test of your of your entire application. So
maybe
something like setting up a test database, right, you want that to be set up for each single
test,
having to manually do that for each test case would become very cumbersome. And then the lines
of codes that you're repeating start to become a serious problem. And that's really one of
the
best use cases for for using fixtures is something like setting up a database or setting up,
you know,
some sort of email service or something that you're going to use across a lot of your tests,
and then having to initialize that manually for each test is going to be is going to require
a
lot of work. And so that's where fixtures really help save us some time. Okay, what I want to
do is I want to create one more test. And this test is going to be a little bit different. I'm
going
to call this test about bank transaction maybe. And so this is going to test us both
depositing
money and withdrawing money in one test case so that we can have a couple of different actions
on a bank account. And so just like we've done before, when it comes to setting up a fixture
so
that we can retrieve an instance of our bank account class, I'm going to do the same thing,
but we're going to use the zero bank account. So it starts out with no money.
And then what we're going to do here is we're going to reference the zero when we're going to
start off by depositing money. And let's say I'll deposit $200. And then we can also
then
withdraw certain amount of money. So now we're testing both in one specific test case, and
I'll
say I'll withdraw 100. And we know that we should ultimately be left with $100. So we'll do a
cert
zero bank account dot balance equals equals 100. So let's test this out.
And let's see how this works. And that works as well. But what I wanted to do is for this
specific
test case, I wanted to show you that we can, we can use fixtures and parameterize so we can
test
multiple values like we did for this specific test case. So what I'm gonna do is I'm going to
copy this so that we don't have to type this all from scratch. And we're going to create a couple
of
different test scenarios for this specific function. And in this case, we're going to
have
a deposited, we're going to have a withdrew. And we can have the expected amount and keep in
mind,
you're not limited to having three variables, you can put as many as you want, it just happens
to work out that I only need three in this case. And then we can provide some test data. So how
about
the same one we have right now, we're going to deposit 100, 200, sorry, withdraw 100. And
we
expect 100 in the end. We can do the same thing. How about deposit 50, withdraw 10. And we
should
end up with 40. And then we've got our high roller, which is sitting there with a whopping
$1,200.
And he went through how about 200. And he's gonna end up with 1000 bucks at the end. So now we
can
pass deposited with drew and expected. And now we can use deposited here with drew here. And
then
we know what the expected is going to be. So let's try this out. All right, and we can see all
three
test cases run. And in this case, all three test cases passed. So I just wanted to show you
guys that you can use this with our fixture. So it's okay, if you're using a fixture, you can still
use
the parameterize decorator in this case to test multiple scenarios. If we go back to our
bank
account class, and you take a look at the withdraw method, you'll notice that right now, we're
not
performing any check to make sure that the user has enough money. And that's not really how a
bank account works. Because if you have 100 bucks, and you withdraw 500, the bank's gonna be
like,
we can't do that. So it just makes sense in our code to actually perform some sort of check.
And so usually, you know, something simple as we'll say, hey, if amount that you're trying
to
withdraw is greater than your balance, well, what are we going to do? Well, we're probably
going to
raise an exception. So I'll say raise. And we're just gonna raise a standard Python
exception.
And we'll just say something in sufficient in sufficient funds in
account.
Right. However, if we do have enough money, then we will actually perform that operation. So
this is all good. If we go to our test calculations, and we just add one more scenario
in this case. But this time, we deposit, let's say only $10. And we try to
withdraw,
I don't know, 50. Well, what is the expected scenario, the expected value, and it
doesn't
really matter what we put in, because this is ultimately going to, you know, break, break our
code, because we're raising an exception. So if I try to run this,
right, it's going to fail. Because we're raising an exception. And remember, anytime you throw
an error, that's going to automatically make the test fail. So what do
we do? Well, first of all, let's remove this test case. Because for scenarios like this, where
you expect to receive an error, we need to create a different test case. So I'll call
this
we'll have a test insufficient funds. And let's say we start out with, instead of using
the
zero bank account, whereas the instead, instead of using this bank account, we're going to
use
the zero bank account, and then we'll just try to withdraw money. So that's an easy way of
testing it. So we'll say zero underscore bank account. So we'll have no money in our account. And
we're
going to withdraw some money. So we would do, you know, actually, I'll just do a regular bank
account. I don't know why I want zero. So we'll start out with 50 bucks. And we'll try to do
a
bank account dot withdraw. And we'll try to withdraw $200. So that should raise the
exception.
But how exactly in our test do we tell Python it's expected to receive an exception?
Because
right now, if we run this, once again, we're going to get the same exact error, right failed.
So what do we do? This is how you tell Python that you accept that you expect an
exception you do with and then you call pytest. Remember, we imported pytest all the way at
the
top right here. So we'll do pytest dot raises. And then you provide the exception that you
expect.
So in this case, this is just a regular Python exception. So you can just say that. And then
you provide the command that you're going to run. Okay, and so that way, if this command throws
an
error or raises an exception, pytest will be told to expect this. So then it would consider
the test
as passing. And now if I run this, we can see that the insufficient funds test passed. And if
we go
back to our code and actually remove this exception, we'll comment out that entire
statement.
What do you think is going to happen here? Right now it's failed, because it did not raise
an
exception, pytest is expecting an exception, and we didn't raise one. So then it considers
that a
error in our code, because when we expect an exception, we should expect an
exception.
So I'll uncomment this. And now, you know, in a in real code, right, you wouldn't just
throw
just a generic Python exception, you're going to usually create your own exception classes. So
let's say we define our own exception. And I'll call this insufficient
funds. And this is going to extend the exception class. And we'll just say there's no
properties,
I'll just say pass. And in this case, I'll say, we'll raise an insufficient fund in
sufficient
funds. Alright, so if I make this change, what do you think is going to happen? You think
it's
going to break our code? Well, let's run this, we can see that it passed. And the reason why
it
passed is because insufficient funds is a child class to the exception class. So when we say
we
accept, we expect an exception, technically, we are getting the exception, it's just, we
haven't specified we specifically want a insufficient funds exception. And because
this
is inheriting from this class, Python says, hey, look, everything looks good. But if you did
want
to be as specific as possible, which is generally recommended, what we can do is I can import
that
exception. So we'll say from app calculations, we can import insufficient funds. And now
here,
we can say we expect, we expect a insufficient funds. And so now if I run this, we can see
that
it passes. And the benefit of this is that if for some reason, our code raised another type
of
exception, it would consider this test as failing. So if I go back to my code, and let's say
that,
you know, for some reason, something broken our code, and all of a sudden, instead of raising
an insufficient funds error, we get some other kind of exception. And I can just simulate
this
by doing raise zero division error. So if you ever try to divide a number by zero, this is the
the exception that's expected. And so now if I run this, right, we can see that the test
failed
because we expected a insufficient funds exception, but we got a zero division error. And so
that
considers the test as failing, because we've told that we've expected something, but we got
something else. So it's generally best to explicitly define the exact exception that you expect so that
you
can make sure that, you know, that nothing else is broken, because technically, any part of
your code can break and throw an error. And if it throws an error, and we just try to, if we just
tell,
you know, our test to expect any exception, then it may not be doing what we actually expect
it to do. But we'll change this back. We don't need that anymore. Okay, so I think we have a
good
enough understanding on how to create a test. And I think it's time we actually started
testing our application. When it comes to fast API, fast API automatically provides us a test client. All
we
have to do is we just say from fast API dot test client, import test client. And then all we
have
to do is then just pass in our fast API instance, which is stored under the variable app. And
then we can just perform any requests to our application by doing client, which is what we named the
test
client, and then just doing a dot get or a dot post or whatever. And then we can get the
response, and then we can assert anything that we want, just to make sure that we get the responses that
we
ultimately expect. And keep in mind, when you make use of this test client, right, if you take
a look
at the documentation, it says that you use this the same way you would any requests object. So
if
you ever use the request library from Python, right, this is the same exact object. So
anything you
could do with requests, you can do with the test client. So you can customize the request, you
can, you know, change the HTTP method, you can add a body a payload, you can add a authorization
header,
it's really just doing what postman is doing for us. But we're going to do it through code
instead of manually opening a postman. And then you know, you know, figuring out which requests we want
to
send, we're just going to do it out in Python. So we can do it all in an automatic automated
fashion.
But if you haven't worked with the request library, definitely take a look at this. Just take
a look at how we generate requests. It's not too difficult, right? It's just request dot get, or in our
case,
we're going to call a client, but it doesn't really matter. And then you provide the URL that
you want to run this against. And then you know, you can add in other properties like an auth header
or
anything like that. So it's just about customizing your specific HTTP request. And so I'm
going to
create a new file. And I'm going to call this how about test underscore users, this, this
module
represent tests for you know, creating users and, and getting users profiles information and
things
like that. And just like it shows in the fast API documentation, let's just copy this line
right here.
And then what we want to do is we want to import from our app package, go to our main
file,
we want to import our, our fast API instance. So we want to import this app right here so that
we
can actually test it. I'll say from app dot main import app. Alright, so we have our app
instance.
And we'll just copy this line right here, which is just setting our test client to a
variable
called client. And so now we can run whatever tests that we want. And I think, you know,
just
to keep things simple, let's start off with the simple route path that we have right here,
just
to test that this works. Because that way, we don't have to worry about authentication or
passing any other data, let's start off with the simplest scenarios. So this is at the root
URL.
So we will go to our test users. And I'm just going to call this test route. Because it's
not
really a real function or anything like that. And then here, we'll say client dot. And since
we are
testing a specific path operation, this is a get method. So we want to send a get so we'll do
client
dot get, then the URL. So we can just say slash, we don't have to do, you know, localhost,
because
it's not technically a running server, we actually just pass the actual app instance. So we
just say the route path. And then you can pass in other things like, you know, what payload and
things
like that. But we'll leave that for now. Leave it empty. And we'll, we'll store the
response
under a variable called res. And instead of doing an assert or anything like that, for now, I
just want to print out the response. All right, and if I run this,
okay, so a little bit annoying thing right now, it's going to run all of our tests. And if you
just want to test one test or one module, then I recommend you just do the same thing except
pass
in the path to just your specific your specific module so that we don't run all of our tests
all
over again. So we can just run one in this case. And if we take a look at this, we can see
that it
just prints out a response object, not very helpful. But if you want to see what's the actual
payload,
we can say dot chase on to see what the data looks like. And so then I can run this. And we
can see
that hey, look, we get message Hello World. And if I wanted to grab the message property, I
could say
dot get. And then message and it should return Hello World. Let's try this out one more
time.
And we can see that it prints out Hello World. So to test that this route actually works, what
we can do is we can just do an assert. And I could say assert and then literally just
copy
this exact message. And I can say, hey, the message should return a value of Hello
World.
It's as simple as that. It's no different than what we did before with our, you know, our
addition function, right? We're just performing some action, you know, running our code and
then
making sure that the results are what we expect them to be. And so now if I run this
again,
right, once again, still passes because it does return a value of Hello World for the
message
property. And we can assert multiple things, right? It's not just one, we can do a second test
case. And I can say res dot and we can access a property called status code is even recommending that.
So
we can say get me the status code. And I could say, well, I expect this to be a, you know, if
we don't
change what the default status code is, it's going to default to a value of 200 as per the
fast API documentation. So this should be a 200, we shouldn't get any other values. So if I run
this,
once again, everything works. But if I go into my code and create a little bug, let's
say,
I just add, you know, an exclamation point or something. Then if I run
this,
we can see that it fails. And that's because we expected Hello World, but we got Hello
World
exclamation. Let's change that back. And this time, let's create a different bug, let's
actually
change what is the status code. And so I can't exactly remember how to do that. So let's go
to
our post router. And we just set the status code right here like this. So if I copy this, and
go
back to here. And actually, was it in the decorator? Yeah, it's in the decorator, you put that
in there.
And we have to import status. And so now if I try this out,
and run this, once again, it's going to fail. And that's because we're asserting. And the
response
was a 201. But we expected a 200 as per our test, which over here says it should be 200. And
it
throws an error. So you can see we could test a lot of different things, you can choose to
test each and every property that you're interested in to make sure that the response is exactly
what
you expect it to be. But at this point, I think we understand how to send requests. So let me
just,
first of all, undo those changes that I just made, just to make sure I don't break anything
else in my app. And we no longer need to import that. So remove that, we'll save everything. And let's
just
run our test one more time, just to make sure that everything's good. And it looks like
everything passes. Before moving any further, I want to show you a couple of other flags that we can
pass
to the pytest command so that we can kind of tweak the behavior of our testing. And the first
thing
I want to do is if you take a look at the output, you can see that we get a whole bunch of
warnings. And, you know, depending on how many packages and libraries that you have installed, you
could
potentially see a lot more warnings. And I actually hate looking at these things. So what I
normally like to do is I usually like to pass in the dash dash, I think it's a disable warnings, I
think.
And so now if I run this, you can see that there's no more warnings. And we just see the
testing information. So I think that looks a lot cleaner. And then the other thing that I want
to
show you guys is, till now all of our, you know, testing when we've broken a specific function
or
class, we've only had one test fail. And I want to show you what happens when you have
multiple
tests fail. So we go all the way to the top in the test calculations folder, and then maybe I
just
change, you know, the subtract function. So I'll change this to be six, so we know that it's
going
to error out. And then also at the bottom, I'm going to test, change this test right
here,
so it returns 56 instead of 55. And so if we run this, and I'm going to do dash v, so we can
see
verbose, but you don't have to do that. But if you take a look at what happens, you can see
that, you know, it runs the first three tests, and then the fourth test fails, and then it just
continues
running through all of the other tests, and then it fails on the next one, and then it
finishes running all of the other tests. So the default behavior is when a test fails, we're going
to
continue running all of the other tests. And this may not be the behavior you want. So maybe
you've changed your code, and you just want to see if we've broken anything. And so you want your code
to
ideally, you know, run through the test, and the first time it fails, you want it to stop, so
that you can, you know, then fix that issue and then run through the rest of the
tests.
Afterwards, or, you know, you can manually run it again, because, you know, right now, we only
have like 1617 tests, so it doesn't take that long. But when you start to write a lot
more
tests, and your code starts to grow, these tests can take, you know, minutes, sometimes more
than that, depending on, you know, are you using an external API, are you using a database, these
all
add a significant amount of time for your test. And so if you just want to see if you've
broken anything, you may want pytest to stop on the first failure. So if you ever want pytest to do
that,
all you have to do is you have to pass in the dash x flag. So this will tell pytest to
stop
after the first test fails. So if I run this, you can see that we get passed, passed,
passed,
and then the first test failed, and then we stop and we don't do anything else. And so those
are just the two flags that I wanted to go over, I don't think we'll really cover any other
specific
flags. I just wanted to go over these two, because they just just happen to be two of the ones
that I do like to use occasionally. But let's make sure we go ahead and change all of our code back. So
I
think we changed this one, this should be back to five. And this should be changed back to
55.
And just to make sure let's just run our code once more, just to make sure we didn't
break
anything in, we could see all 16 tests pass. So now that we know how to simulate a request to
our
API, let's create a test for testing the user create functionality. So what we're doing is
we're
going to create a user. And we're going to test this specific route right here where we create
a
user. So let's try this out. We're gonna do the same thing. I'll call this def test
underscore,
create underscore user. And here, I can say res equals client, like we did before. But this
time,
it's going to be a POST request. And the URL, that's going to be slash users. All right. And
then
since we're now sending a request, you know, to create a user, we have to send data in the
body.
So to send data in the body, you say JSON equals, and then you pass in a Python dictionary. So
this
is going to get sent as the body. And if you go back to our specific route, which is right
here,
you can see that we expect the users create schema. So if we take a look at the schemas, you
can see that somewhere in here, whereas user create, here we go, we expect an email and
a
password. So let's send an email and a password. And so for email, we'll say this is going to
be
hello 123 at gmail.com. And the password is going to be password 123. And just to see that it
works,
let's do a print. And I'll say res dot JSON. And then the test requirements that you
know,
how do we verify that it works? Well, let's start off with something very simple. So we'll say
assert. And I'll say res dot status code. We know that when we create a user, we expect a 201.
And
we can just verify that by taking a look at the path operation. But I think this should be
good. And then now if I run this, and I'm going to run just this file. So I'll say test test
user.
And let's see what happens. So we see both of them passed. And how do we verify that?
Well,
I mean, we should see this user in our database, right? So if I go to my PG admin, go to my
database,
and we'll go to our schemas, tables, and go to users, and we'll do a view edit all. All
right,
we can see the Hello 123. So it was successfully created. I'm just going to quickly delete
this
again. Well, actually, let me show you what happens if we don't. I'm actually going to do
something
unique here, I'm going to say, well, actually, let's just do an assert. And I'll say, res
dot
JSON dot get. And we want to get one field just to verify that it's correct. So what's the
data
going to look like when it comes back? Well, we should see a print, but I didn't. That's
because
I forgot to do the dash s flag. But I'm going back to the route, we're going to send the
response as
a user out. So we expected to match this specific this specific model right here. So it should
have
an ID, an email and a created at. So let's add that in there. I expect the email to
be
two equal equal the email that I gave it. And you can really do as many, you know, asserts as
you
want. So you could verify each single property has been set, you can verify that an ID has
been set,
I'm just going to keep it simple, we'll just say just the email, we'll just run that check.
And so we'll run it again. And I'm going to do the dash s flag as well. All right, and we see that it
failed.
And we can see that it failed because there's a duplicate key violates unique constraint of
email. And that's because this user was already created. So I can't recreate him. So let me delete
this.
And I don't think I yeah, if I select the number and then go to Oh, no, it doesn't look like I
can.
There we go. Now once I select that number, I can delete this user.
And then we could save that to database. And so now if I run this again, we should see it
work. And we can see that both tests pass.
And we can see that both of our certain statements were checked and validated, and they both
looked good. But instead of doing it like this, we can even take it one step further.
Instead, what we can do is I can actually import this user out schema into my test. So I can
say,
from app dot schemas import user out. And then I can remove this print
statement.
And what I can do is here, I can actually use the schemas. Actually,
sorry,
I'm actually going to change this import, I'm going to just import all schemas. So say from
app, import schemas. And now I can actually create a new
pedantic model, and I'll say schemas dot user out. So we're going to create a new
schema,
save this as a variable, I'll call this new user. And we can pass in the data that we get back
from
the response, I can say res dot JSON. And remember, we have to actually unpack the dictionary.
So it's
in the right format to create a new user out model. And now this is going to perform a certain
level
of validation. So it's going to confirm, hey, do we have an ID? Do we have an email? Do we
have a
what was the other thing that we needed? And do we have a created app? So it's going to make
sure it's got these three things, it's not going to check to make sure that it's the right
email,
it's not going to check to see if the right ID, but it's going to check to make sure that at
least it has these three properties. So it automatically does some of the validation for us. And
it'll
throw a an error if one of those are missing. And if it throws an error, then obviously,
pytest will consider this test a failure. So by doing this, we've kind of, we've kind of
saved
us a little time. And now I can actually change this instead of doing this, I could say, new
underscore user dot email equals this value. So now if I run this, hopefully this
works.
And it looks like it failed. And let's quickly see what happened here.
Oh, once again, I forgot to delete the user beforehand. I will delete him and then
try
this test one more time. And they passed. So I think this is what we're going to do
moving
forward, we're going to make use of our pedantic models to do some of the validation for
us
automatically so that we don't have like, you know, 60 or seven, probably not 60, but we won't
have 510 different assert statements, which you can absolutely do. And you probably do want to,
but
this will remove some of them, because it's at least checking to see if some of them are
there. So before moving any further, you know, we need to address something important. And that is
that
right now we are using our development database for our tests. And that's not good. I don't
want
to use my development database, I want to create my own separate database to perform testing
on
so that I don't get in the way of development, or staging or any of our production
databases,
I want to create a completely separate database for testing. Right now, when we, you know,
import our client, right, we're going to be using whatever the database is that we've
defined
in the rest of our app, which is going to be our fast API database within PG admin. And we can
see that right here. And so that's why I'm having to go in and, you know, make sure that it's
created,
and we've already got some other data which could potentially break our tests. And so I want
to make
sure that we have a completely separate database for testing so that it does not get in the
way of anything else. So how exactly do we do that? Well, one of the great parts about the way we've set
up
our database is if we actually go to our database.py file, right, you can see that we've
actually set up a date, a dependency. So when we set up our dependency, you can see we created this
function
called get DB. And this get DB actually returns the session local. So this is what allows us
to
actually, you know, make queries using SQL alchemy. And that comes from this right
here,
from the session maker, where we passed in all of our database connection details. And if we
go
into any one of our routes, it doesn't really matter which one, you can see that we pass in
the dependency into the route. And the great part about having dependencies is that it's very
easy
to override a dependency in any environment, especially a testing environment, we can say,
hey, instead of passing in, you know, get DB, we can say, you know, something like,
get, I don't know, underscore test underscore DB, which is going to be a function that returns
a test instance of our database instead of our development instance of our database. So
I'm
going to show you guys how we can set that up and how easy that is. So we'll go back to our
test
users. And we're essentially just going to copy all of this right here. So I want you to
copy
this all the way up to the URL. And we'll have to set up some imports. And we'll come back to
that
in a second. So let's create some space. Alright, so we've got all of this, and we need to
import
a few things. So we'll start off by we'll have to import settings. So we'll say from app
import.
Actually, we'll say from app dot config import, I think it's settings.
Yep.
Alright, so we've got that cleared. We then have to import create engine. And so we'll go up
here and I'll say from SQL alchemy, import, create underscore engine.
And then we need our session maker as well. And I think if I hover over this, can I? Nope, it
doesn't look like it'll auto import that for me. So I'll say from SQL alchemy
dot or m import session maker. All right, looks like we got most of the issues, we still
need
declarative base. And we'll import that. I'll say from SQL alchemy dot ext dot
declarative,
import declarative base. And if you guys don't know where I'm getting this, I'm literally just
copying this straight from this file. So everything that's here, I'm just you can just copy it as
well.
And if we go down, I think we have everything, right, we no longer have any errors. So we've
got all of our imports. And so this is the function that returns our session local. So this is
what
allows us to query the database. This is our session object, right? And this is how it's set
up exactly in the database.py file. So what I'm going to do is I'm going to create another
function,
but this time I call it override underscore, get underscore DB. And this is going to
return
this session local instance. But I'm going to rename this just so I don't get it confused.
And
I'll call this testing session local. And then I'll reference this. All right. And so that
means
right now in our current application, we're referencing the one that we created in our
database.py file. But the one in our test, we're going to actually make use of this new one
right
here, if we call this override, get DB. So how exactly do we override the database or any
dependency
really? Well, fast API has a section on here. And I think if I search for
dependency,
testing dependencies with overrides. Right, to actually set up the override, you just do app
dot dependency override. And then you provide
what you want to override it with. And there's actually a better example. If I go to testing
database, this will actually show you how to do that. So we've got the override DB, where
we've
created a different session object. And to override it, we just do app dot dependency
overrides,
we pass in what we specify the dependency we want to override, which is anything with get
DB,
and then we want to override it with override underscore get DB. So essentially, all that's
really doing is in the functions, any of our routes, right, you can see we've got this
depends
get DB. So when we run that command right here, this override, all it's going to do is it's
going
to swap this out with the function that we have right here under test users, called
override,
get DB, which is just going to give a different session object. And this session object can
point to another Postgres database. That's all we're doing. It's really that simple. That's why we
set
up that, that dependency when we first started configuring our database so that we can
easily
test this during testing, because we can just use the override functionality that's used
within a fast API. So I'm going to copy this. And right here, I'm going to paste it down here. And I
realize
this I misspelled override. But we do have to import get DB now. So let's import
that.
They'll say from app dot database, import, get underscore DB. And so it's going to
swap
the dependencies out. And since we're still referencing the app object, we're going to
pass
that into test client. And so now, anytime we use this client, it's going to use the new
database.
Now, currently, our database is using the environment variables, which once again, is going to
point to our development database. So there's a couple of things we can do.
Since this is testing, we 100% could just hard code our URL, because I mean, it's our
testing
database, who cares if our password and our information is already out there. So we could
technically do an F string. Actually, we don't even need an F string, I could just
say,
Postgres queue, whoops, there should be a string. Postgres ql colon slash
slash,
then we have the username. Right now for my, for my database, it's just
Postgres.
And then we have the password, which is password 123. Remember, this would technically be for
your
testing database. So it's not technically a problem if you put this in there and hard code it.
However,
you'll see that we could just use the environment variables and make a small tweak. But I'm
just going to quickly write this out for you guys if you wanted to see it. And then the hosting
is
gonna be localhost ports can be 5432. And then our database name, so we could call this, you
know,
my print, my development database is called fast API, I could call my testing database, maybe
fast API underscore test. So whatever my project name is, and then underscore test, you can call
it
whatever you want. And so I could just hard code that and then comment this out. Something
like
that, or I could use this same thing, but just do a underscore test here. And so that way, I
don't
have to hard code anything, it's going to actually pull it from the environment variables. So
whichever method works for you, ultimately, that's all that matters.
Now, I technically already have a fast API test database. So I'm going to actually delete
this
for now, we'll say delete drop. Alright, so I'm now in, however, your Postgres instance set
up,
but since we're going to be using a database called fast API underscore test, we actually have
to create one. So I'll say create database. And I'll say fast API underscore test. Okay, so we've
got
our test database created. And we've set up the override. Let's now actually test this out.
So
we're going to do one last thing. And I realize, since we have a brand new database, there's
no
tables, right? If I go down to schemas tables, right, there's nothing there. So we actually
have
to build out our our tables before we actually do anything. So easiest way to do that is just
copy
was my main file, here we go, this command right here. So this will tell SQL coming to build
all the tables based off of the models, we could use alembic as well. So I'll show you guys how we
can
kind of get alembic set up in our testing database so that it actually performs the
migrations
before test. But for now, we're going to keep things simple. And we're going to use this. And
so I'll just paste it right there. And we can remove this models, I don't know why that's
there.
And you can see that we get a warning right here. So we don't have anything based. So we
actually have to import that from our database file. So I'll say from app database, import
base.
And I realized we actually don't need this line right here. So we'll remove that. All
right,
so now our test database is completely set up. And when we when we actually run our first
test,
it's going to run this right here, which should create all of our tables for
us.
So let's take a look at our test create user. Everything should be good. We shouldn't have to
touch anything. And our URL should point to fast API underscore test. So let's try this out.
And
I'm just going to refresh this. And yep, we shouldn't have any tables. Good. Perfect.
And
let's run this now. But let's run just the test users. All right, and both of them passed.
So
how can we verify both of them passed? Well, let's go to our tables, I'm going to refresh
this,
we can now see we've got all three tables, right. And that's because we ran this specific
well, I'll document this command right here. And if I do a users, and then where is it? If you edit
a
all data, all rows, right, we can see that our user was successfully created. So we have
now
gotten our test database set up so that we no longer interfere with our development
database.
And keep in mind, right, it does your test database doesn't even have to run on your local
machine, you can run it anywhere, you can run in a Docker container, you can run on a remote
machine,
you pass in the details of your test database, and then your testing environment will handle
the rest. So now that we've got our test database, and we've successfully ran our tests, if we run
our
test one more time, take a look at what happens. We get an error, right? And we get an
error,
because we saw this before in our development database, we already have a user. So we get this
duplicate key violates unique constraint, because this user already exists. So when we run
this,
again, we try to create a new user with the same exact email. And this obviously causes things
to break. And so how do we get around this limitation, right? I want to be able to run my test as
many
times as possible. And I wanted to kind of start out with the clean slate. Well, we can make
use of fixtures. So let's take a look at our fixture. And if you already forgot what a fixture
was,
it's just a function that runs before your test runs. I'm going to create a fixture. And I'm
going
to have to do we already import pytest? We did not import pytest. So let me import that in
here.
We'll say import pytest. And I'm going to say, I'm going to create my
fixture.
Call this whatever you want. I'm going to call this client. So this is actually going to
return
is actually going to return our client, right? This is our client object right here, which is
the test client, which is what we use to actually generate the request. So I'm going
to
call this def client. And what this is going to do is I'm going to remove this line right
here
and just copy this. And I'm going to say return this. And we can just remove this line at
that
point. So this is going to return our test client instance. And so now to get this to actually
work,
I'm going to pass in our client. And so now this client object references the client that we
get
from this fixture. And we'll do the same thing here. So what exactly has this solved? Right
now,
it hasn't really done anything for us, right? Our code is going to work exactly the same. You
know, if I save things and run it again, right, same exact issue. And obviously, I can go
in,
and I can delete this. And then run this, right, it's going to work just fine. But it is
nice,
because we now have this function that runs before each one of these tests that gives us our
testing
client. And what's even better is I can actually change this a little bit. Because I can
change
this to be a yield. Right, when you change the yield, what it does is it gives us a little
bit
more flexibility, because now we can, you know, run our code before we return our test
client.
And after the yield, we can then run our code. Actually, this should be run our code before
we
run our test. And then this is going to run our code after our test finishes. So now we can
put
in logic for you know, what should we run before our code runs or before our test runs, we
then
yield, which is the same thing as return, it's going to return our test client. And then we
can run some code after the test finishes. So what we can do is before our code
runs, I can copy this line right here. So before our code runs, I can create my tables. And
then
after my code runs, I can drop my tables. If you want to drop a table, you just run based on
metadata
dot drop all instead of create all, and it's going to drop these tables. And so now every
single time
my tests run, I'm essentially going to be starting out with the clean slate, it's going to
drop all of our tables, all the data in each table, it's going to return the test client, we're going
to
run the test, and then sorry, it's, I messed that up. So when we when we finish a test, we're
going
to drop all of our tables, delete our data, then the next time the test runs, what's going to
happen is we're going to create all the tables again, because there's no tables, we're then going
to
run the test. And then just like we did before, we're going to then drop all of our tables
again.
So what I'm going to do is, first of all, I'm going to manually drop these. And I'm going to
have to drop votes first, or that's gonna throw some foreign key errors. And
then we'll drop posts. And then users. Okay, so now everything should work. And I'm actually
going
to test this out. So once again, just to reiterate, we're going to create our tables, because
we don't have any, we're going to then run our test, right yield just returns our test client to our
create
user. And then we can, you know, perform all this logic. After that's done, we'll then drop
the tables again. So we start from scratch the next time around. So let's try this out. All
right,
everything's worked. And if I actually go back to here and hit refresh tables, you can see
there's
no tables, because we dropped the tables after we're done. And if I run this again, it
works,
we can run this as many times as we want, we don't have to worry about duplicate users,
because we're clearing out our database. And there's one more thing that you can do as
well.
If you wanted to, we could actually take this drop all and move it up here. So what's the
what's the
benefit of doing like this? Or why would you want to do it like this? Well, the nice part
about this is right, it's going to clear out anything we had previously, it's going to create our tables,
and
then it's going to run our test. And then the next test that we run, we're once again going to
clear out our table. Yeah, we're going to clear it out. And then we're going to build our new tables
again.
So this way, if you if your test fails, you can, you know, pass in the dash, you know, x flag
to
stop after it fails. And what's going to happen is it's going to keep all your tables and all
the data. So you can see what was the current state of your database, when the test failed, so
that
you can, you know, ideally have a little bit of better idea of what's happening, troubleshoot
a little bit more, because you have access to your, to your tables. Whereas if it's if this is
down
here, right, it's just going to delete the tables after you're done, you can't even see it. So
it's a matter of personal preference. But I like to keep the tables after it's done. So that you
know,
if I do want to stop it and just kind of poke around and take a look, then I can. And if you
guys don't want to use SQL alchemy, you can do the same thing with alembic, right, I can, I
can
import, you know, alembic dot config. Actually, it would I think the technically the import is
from
alembic dot config, import config. Sorry, not that. Nope, nope, it's a from alembic import
command.
So what we can theoretically do is, in here, instead of using these, we could say something
like,
I think it's command dot upgrade. So you can upgrade. And then you can go up upgrade
to,
I think, head, right. So this will build out all the tables for you. And then after you're
done, you could then go in and say, command dot downgrade to base or something, right?
You'd
have to make sure you set up alembic with all of the right environment variables and things
like that. But that's all you would have to do if you didn't want to use SQL alchemy's built
in
functionality. So I'm going to just delete that just to keep things simple. Now, we're going
to
let SQL alchemy build the tables for us moving forward, just because it's a little bit quicker
than having to, you know, build all of your revisions and things like that. So one of the
cool
things with fixtures is that we can actually configure a fixture to be dependent on another
fixture. So we can essentially pass one fixture into an argument of another fixture. And the
reason
why I want to do this is I want to have one fixture that returns my database object. So if I
want to
ever manipulate data directly, and I want another fixture that returns my client, so this,
this one
is returning our client. So I want that. But I also want another one that returns just our
database
object. So what I'm going to do is I'm actually going to create a new fixture. I'll say
pytest
dot fixture. And we can call this whatever you want, you can call it, you know, like DB or
session, I'm just gonna call it session. And I'm actually going to move the logic of deleting and
creating
our tables up to here, because this is kind of handling all the logic for the database. And
you
see all of this override functionality, what I'm actually going to do is I'm going to just
copy this section right here. And paste it into here. Alright, so this session fixture is going
to
yield the database object. So the database object that we used to query things. And what I can
do
now is I can pass in session, the session fixture into my client fixture. And so by doing it
like
this, what's going to happen is any time I go to one of my tests, and I pass in the client
fixture
as a essentially as a dependency, it's going to call this one. And the client fixture will
actually
call the session fixture before it runs. So it's intelligent to understand that a requirement
for
the client fixture is the session fixture. So we're going to run all this code, it's going to
return the DB object, which is going to be passed into client as session. And then here we can
perform
all the logically that we normally would. And I'm going to once again, copy this override DB
right
here. And we can remove this. And we can remove that. No, actually, sorry, I didn't mean to
remove
that. Just space these out a little bit. So it's part of the override get DB function. And
here
instead of yielding DB, since there's no DB, we're going to yield session, which is the DB. So
we'll say session. And then session dot close. And I'm going to copy this line right here. Do the
override
right here. And then we return a brand new test client. Okay, and the benefit of this is that
not
only do we get access to the client, if we wanted to, we can also get access to the database
as well.
So I could say session. And then I can go in here and I can make queries like session dot
query,
you know, models dot post, blah, right? So now I have access to the database object as well.
And
so I can make queries and I have access to the client. But if I call just the client,
remember,
the session database is a input argument into the client fixture. So the session will always
run
first. And then client will run because we're calling it right here. And then we just moved
the override get DB function into here as well as the override right here. And then as
usual,
we return the test client. So let's make sure we didn't break anything. Let's test out our
code. And something's happening with my terminal. So let me open a new one.
So let's try this again. I'm just going to do py test, test slash and then test underscore
users. Nope, test calculations, users.
All right, and to pass so perfect. Looks like we didn't break anything. And we've got a much
nicer
way of setting up our code, we can actually remove this override that's not within a fixture,
we no longer need that. And we no longer need to create all right here outside of any of
the
fixtures, because we're doing it all within the fixtures now. And once again, I just want to
make
sure I didn't break anything. So let's test that out. And we can see to have passed perfect.
All
right. So right now, our test users file is a little bit cluttered with all of this database
information. And I would like to move all of our database specific code to another
file.
So under our test folder, I'm going to create a new file, and we can just call this
database.py.
And I'm going to essentially move all of my database code to that file. And that includes the
fixtures as well. So these two fixtures, all of this, actually everything, we don't need any
of
this in our test users file. And we can just paste it into here. Now this is going to break
things
because obviously, it has no idea what scheme Okay, well, actually, before we do that, we do
need
to have schemas in here. So let me find where schemas is, and we'll just cut that out. But
the
other issue is, is that we no longer have access to the client fixture. So we have to import
that
so I'll do from current directory database import was a client, I think it's client. Yep. So
now we
have client. And remember, we don't need to import. Actually, we may need to import
session,
let's just double check to see if this causes any issues. So let me save
everything.
And let's test this out. Let's see if this works.
Looks like we got two errors. Yep. And if you take a look at the errors, it says the
session
fixture is not found. So we actually have to import the session fixture from this
file,
even though we don't actually call session in here, because client is dependent on
session,
because client is ultimately calling session, we need to make sure that session is also
imported.
So we have to do session here as well. And so now I think this should fix our
issue.
Now, if we run this, yep, both tests have successfully passed. And if you actually take a look
at our test file, you can see our test file is a lot cleaner, it's really just got our
two tests and a couple of imports. And before we move on any further, there's one thing I want
to
point out. And I ran into this issue when I was, you know, run doing a dry run through my
code.
If you actually take a look at your routers, and we'll go to since we're taking a look at
test, create user, this is going to be this slash users. So we go to users and then create
user,
right, you can see that the prefix is slash users. So the route you want to go to is slash
users.
And then you can see that we have to append whatever is here. So here it's appending
slash.
Now, the way that pytest works is that or actually the way that anything works is that if you
actually
send a request to slash users, it's a little bit different than slash users slash. And I'm
actually
going to show you what happens when we do that. So I'm going to, first of all, open up a UV
coin set.
So I've started, I've started out my API. And if we go to create user, here, I'm going to
users and
not users slash, right? I'm not doing slash, I'm doing slash user, I'm just doing users, which
is
where did it go? Here? Where did I put my the things I just typed? Let's
Yeah, here. So what we're doing is we're sending a request to this one, not the one above it,
because the other one above it has a slash at the end,
we don't have that trailing slash. So if I create a test user, without that trailing
slash,
hit send. Take a look at what happens in the logs, it says that when we get a request to slash
users,
we're going to send a 307 redirect to slash user slash, because technically the routes on
slash
users slash, and fast API is intelligent enough to actually send the redirect to here.
However,
this creates an issue with our testing. Because if I just do slash, what's going to happen is
when
I do this res dot status code, check, it's going to check for 307 and not the final 207 that
the
final 201. And I could show that to you guys. So let me go down here. We'll run our test real
quick.
And you'll see that it fails. And it fails because well, there's a couple of issues, it fails,
it's it says it fails because our validation for a
schema failed. But I'm going to skip that and comment that out. And I'll comment this line up,
I just want to take a look at the status code. Because obviously, if there's a
redirect,
there's no information in there. And you can see that this fails because the response code is
307.
So that's why you want to make sure you add that trailing slasher, then you'll run into some
issues.
So we've got our create user test pretty much done. You can feel free to customize it
however
you want. If you want to add a few extra assert statements, feel free to do that. But we're
going to move on to login. So let's actually handle setting up the login. So I'm going to do
def
test underscore login, underscore user. All right. And this test is going to be dependent on
the
client fixture, because we need to be able to send a request. So I'm going to pass in client.
And I'm actually going to copy this line right here. We'll change it, of course, but
all right. And so we're going to this is going to go to slash login. And there's no trailing
slash
on this one, just because if you actually look at our specific route, for login, I think it's
under auth.py. You can see there's no prefix here, it's just slash login. So we don't need the
trailing
slash on this one, you just have to figure out how you set yours up and make sure it matches
accordingly.
We do slash login. But in this case, we don't want to send this data in the body.
Instead,
remember, if you go to postman, and you go to login user, we don't send it in the well, we do
send it in the body. But instead of using the normal,
normal JSON format, we have to send it as form data, remember. And so to change this to be
form data, you just change JSON to be data. And we've got the same
credentials. So this should work. And let's just quickly just do a assert. And what is a
login
return? A login returns a it should just be a 200. So we'll do a 200 assert res dot status
code
equals equals 200. All right, and let's test this out now.
And it looks like we got an error. So what happened here? Let's take a look. It
looks
like we got a 422 as our status code, why would we get a 422? And if we take a look at our
logs,
we don't really get much information here. So what I'm going to do is we're just going to
print
res dot JSON. So we can see what's going on. Why did we get a 422? Is there any other
details
that's going to provide us? So I'll do a dash s. So it prints it out. And we can see in the
print
statement, it says, Oh, look at this, there's some sort of validation issue detail, block
body
username message field required type value error missing. So remember the the field for the
email
when you log in is not actually called email, it's called username. So we have to fix that. So
it's going to be username. All right, now let's test this out once more.
All right, we get another failure. So let's see this time we get a 403. Okay, interesting. So
what's happening? Why do we get a 403? Let's see. detail says invalid credentials.
Interesting.
So if I go back to my login route, right, you could see that we get a, a 403 if the user
doesn't
exist, which means we couldn't find a user with that specific email, or if the password that
was
provided doesn't match what's in the database. So this is one of the issues, it's either the
user doesn't exist, or the email doesn't exist, or the password is incorrect. So let's go
into
PG admin. And if you left your, your database config, so that you don't drop it at the
end,
instead, you drop it before a test case starts, you should be able to see all of your test
data. So I'm going to refresh this. And if I go to users, view, edit data, all rows, right,
there's
no users. Well, that's interesting, right? Because if I go to my code, during our test create
user,
we should have created a brand new user. So why can't we log in as this user? Well, that's
easy
to explain. So if I actually go to my, my database.py, we have these fixtures. And the
fixtures have a
specific scope, which means when do they run? And right now, they're using the default
function
scope. What that means is they're going to run for every they're going to run before every
single
function that's dependent on them. So that means when we run test users, right, we can see
that
test root client, right, since we're passing client into test root, we're going to run
our
fixtures before we run this function. Then before the next test, we're going to run our
fixture
again. And then before this test, we're going to run our fixture again as well. So why does
that
actually cause the user get deleted? Pretty simple, because we run this test, right? The test
create
user, the users in the database, then we start to run the test login user. And so since
we're
dependent on the client fixture, we go to the database.py file, we can see that the client
calls session, session, what does session do? Well, it drops the database, or it drops all
the
tables, at least. And then it creates a all of the new tables. So we get a brand new
database,
every single test that we run. That's why the user isn't there. And if you want to learn
more
about scopes, I recommend you go here. And then I think I go to API reference, I think. And
then
fixtures, not no, let's go to examples. And go to fixtures right here. And then here I can
search
for scope. Here we can see the different scopes. So the default right now is that our scope
is
functions, which means that the fixture is destroyed at the end of each test, and it's run for
every single function. We then have a fixture that sets to a scope of class, which means
that
it'll run once per class, and it'll run all the tests in the class. Then we have a fixture
module.
So when a fixture is set to a module, that means for all of the tests in a specific module,
right, like in this case, all of the tests here, we're going to run the fixture just once at the
beginning.
And then we're not going to run it again, for that module. So all of the tests will have
access to
just that first instance run essentially. And then we also have it for package where it's
going to
run once per package. And then we also have session where it's going to run once at
the
start of the testing session, and it's going to get destroyed. And you can see how we can set
the
scope right here, you just do scope equals and then whichever one you want. So if we go back
to
here, and we see this, this fixture, right, we could say, since the default is
function,
it's the equivalent of doing scope equals function. And the same thing goes for
the,
the session, the database one as well. So these are going to run and get destroyed every
single
function. So at the end, the function, we're going to delete this test client, and then we're
going to create a brand new one for the next one, which causes our database to get wiped and
destroyed.
So if we want our login user to be able to access what was created here,
we could theoretically change the scope to be module instead. And I could change this for
both.
And so now what happens is, at the start of this module, when we start running the test
here,
we're going to run the fixture, right? Because the test root is calling it. So we'll run the
fixture at the beginning. And we're going to just do it once, right? So then we all you know,
we'll
call this one, which doesn't really do anything, we'll then create the user. But since we
don't ever run the fixture, again, we never end up running this session either. So we don't end
up
dropping our database or our tables. So we'll actually keep that same fixture. And then
we'll
run through all of our tests. And only at the end, will we destroy that fixture. So we have
essentially access to all of the the tour database, the end for the entire module in this
case.
And so now if I actually run this right, you can see all of our tests pass because the
fixture
will last the entirety of the module, I could even use package or session, both of them
would
have worked. But this creates a little bit of an issue. Because this means that this
test
is dependent on this test completing. So if this test fails, then obviously this test is going
to
fail. And it actually creates issues. Because if I move this to the top, right, and I
accidentally
move my login user to before test create user, then this is going to break our tests once
again,
because we need to create the user before we log in the user. And so in general, what we've
done is
bad practice, you don't want to make tests be dependent on other tests, each and every
single
test needs to operate and run independently, they should not be reliant on other tests. If
they are,
it is not a valid test. And you're doing it wrong. You want to make sure that all tests can
run
independently of one another. So what we've done so far with this little hack of changing the
scope
to be module is not a good solution. So in the next video, we'll take a look at what we can
do
to kind of overcome this limitation, this issue, so that we can make our login test case
independent
of our create user test case. Okay, guys, I wanted to make sure that, you know, we really
did
understand what scopes are when it comes to fixtures, because I feel like I didn't do that
good of a job explaining it. So hopefully, this video will help explain it a little bit
more.
And all I'm going to do is under our session fixture, I'm going to just do a little print
statement right here that says my session fixture ran. And this is, you know, I'm recording
this
video in the future. So I've got all of the other tests that we're going to cover done
already. And I've kind of broken it out into test posts. This is testing all of our post path
operations,
we've got one for users and one for votes. And I'm going to show you guys what happens when we
use the default scope, which is it's scoped per function, right? This is the equivalent of
doing
scope equals function, which means that we're going to run this fixture for every
single
function that requests it. And so if I remove this, because once again, it's the
default,
and I run my tests, I want you to take a look at what happens.
For every single test that we run one dot represents the test, we can see it prints
out.
And so it's running through all the tests and tests underscore posts, and then it's going to
run all the tests under in test underscore users, and then votes. So you could see that
because
this is set to function, we're going to run this entire session, fixture this
function,
every single test that we have, or at least every single test that's requesting this
specific
fixture. That's why we see it run every single time. Now, if we change the scope to
module,
the behavior is going to be a little bit different. Now, if I change the scope to module,
what's going
to happen is that for every module that has a test that's requesting this specific session
fixture,
we're only going to run this fixture one time per module. And so now if I save
this,
and I'm going to no longer do dash v, I'm just going to do just dash s, so we can see this
get
printed out without too much extra data, because we're going to see a lot of tests fail. And
that's perfectly okay. I want you to notice what's happening. So notice how the print statement
ran
here. And then the print statement ran here. And I had to cancel out of that, because usually
I end
up with a ton of errors, and then I lose my data. But you could see that the first module,
which is
tests underscore py, we could see that my fixture ran because it's because I told it to run
because
one of the tests is requesting it, it runs, then one of the tests runs. And then the next
test
runs, and then the next test runs. So you could see that this is not printing out per test,
it's only printing out one time per module. So when tests underscore posts, py ran for the
first
time. And the first test required this specific fixture, it ran once, then it didn't run for
any
of the other ones. That's why we see them all fail. And then the next module loads up and we
start running tests from there. So we run the fixture just one time. And then we don't run
it
ever again. So you can see seven other, I think it's six or seven other tests ran with no
other
modules with with the fixture not running any other time. And then we can see the same
thing
for votes, we can see that my fixture ran just one time. So this means that it's going to
run
only once per module. We also have session. So if I do session, it's going to be even more
different,
you're going to see it run only one time for my entire testing session. So you can see my
fixture
ran, it's going to keep going through, which is perfectly fine. And then when we get to the
next module, we should see it not run at all. Right. And so you can see it didn't print again. So
it's
going to go through all of the different modules. And I'm going to cancel out of that before I
get too many errors. Right. But now it only runs once per entire testing session. So that's when
the
scope comes into play. So keep in mind, you know, depending on what you're trying to
accomplish, you may need to tweak the scopes of your fixtures, depending on what they're supposed to be
doing.
But for our database, we want to keep it set to function, which is the default value, because
we
want to load up a brand new database by dropping our tables and then creating new ones every
single
time we run our test. So the default works perfectly fine for us. Now, to make our
login
user test independent of any other test, so it's not reliant on a specific order or setting
the
fixtures to be of a specific scope. What we want to do is before our login user test runs, I
want
to actually go in define a function that will get called, that will actually create a brand
new user so that we can actually test the login user out. And so, you know, as soon as I said the
words,
I want to run a function before test runs, the first thing that should click in your head is
you want to run a fixture or you want to create a new fixture. So we can actually create a fixture
that
will create a test user for us. So up here at the top of this file, let's create a new
fixture. And
I'm actually going to delete this test, we don't actually need it anymore. It's just taking up
space, or I can at least comment it out, I guess. And I'm going to define a fixture here. And
we
have to import pytest. So we'll say from import pytest. And we'll say at pytest.fixture. And
I'm
going to call this test user. So it's gonna be responsible for creating a test user. And this
is
actually going to make a call to our create user route. So we actually need access to
client,
so that we can actually make that request. And there's two different ways of doing this,
right? We could just have access to the session object, we can have access to session
object,
and we can just, you know, session dot, you know, I think what is it, I actually know it would
be
like models, dot user, and then pass in the data for a test user, and then create it directly
with
the database. Or we could just make a call to our API and actually have it run through that.
So I'm going to do that because it's a little bit simpler for now. But I'm going to define a a
dictionary
with the data that I'm going to use for my user. So I'm going to have the email. And
actually,
it's going to be a little bit easier if I just paste this in. So we have the email set to
something
and then the password set to something. And I'm going to change this back to
client.
And now I can say res equals client dot post, and we'll send it to
users.
And then the data for the new user can just be user underscore data. And just kind of like
what
we've done here, we can, you know, just to make sure that this didn't error out, we can do an
assert and just say, you know, res dot status code equals equals to a one. If that's
good,
I think it's safe to assume we didn't run into any errors on on the back end. But then what I
want
to do is not only do I want to create the test user, I want to actually return the
information
about the user so that this login user will have the correct data to send when he tries to log
in.
That way, if we ever change this data, we should automatically update this
data.
So what are we going to return? Well, we have access to what is it called to res dot
JSON.
And if I print that out, we can just quickly take a look at what that looks like. And for
now,
I'm just going to return nothing, I guess that's fine, I'll just return nothing. And here I'm
going
to change this to be, I'm going to pass in the test user. So that also runs. And so now
this
login user is dependent on two different fixtures, client and test user. And before we do
anything
else, let's make sure we clean up the scope, we don't need these to be set to that
anymore.
And if we run our tests. All right, the data we get back, we get the
username.
I'm sorry, that's not what I want this print, we get the ID, the email and the created
at.
So what we'll do is I'm going to actually take this dictionary, and I'm going to pass in the
password as well. Because when I actually go to log in the user,
I need to make sure that he has access to what my password is. But that doesn't get returned
in the response. So what we have to do is, we'll have to add a new field. So I'll say new
underscore
user equals, you know, res dot JSON. So we get that dictionary, and then we'll say new
underscore
user, we'll add in a new key called password. And I'll pass in user underscore data, which is
this
dictionary up here, and we'll grab the password from here. And what I'm going to do is then
return
new underscore user. And so anytime someone calls test user, or anytime we have a test
that's
dependent on that, it's going to run this function, it's going to return the new user. And so
now that
we have the new user, what we can do is I can say the username is test underscore user. And
then
we'll grab the email key, and then the password will be test underscore user. And this will
be
password. Okay, and so that way, if I ever change the password up here, it's going to
automatically
get updated down here. So we don't ever have to worry about that. And one thing to note is
that
technically, if I deleted this and just call a test user, right, test user would technically
call
client. And that would go all the way back here. And then client would technically call
session.
So it would call all of these functions automatically for us. However, the one issue
is
that in this test, we still need to make a call with clients. So we still have to pass in the
client object so that we can make use of it. But I did want to highlight that because
this fixture is dependent on client, it will run whatever code is in the client fixture. And
because
client is dependent on session, it will run whatever code is there within the session. So it's
all done automatically for us. Right. So the order once again, is going to be we'll
run
session first, we'll then run client, and then we'll run this test user. So let's test this
out. Now. Hopefully, this works. And it passed perfect. Look at that,
guys, we were now successfully able to log in a user. So that's cool and all. But I want to do
a
little bit more validation for the login user route, just to make sure that you know, we got a
token,
the token is valid, and just do a few extra checks. So just like we did with the test create
user,
we have we can take use or we can make use of the pedantic model that we use for the response.
So
if I go into my auth.py under routers, and we can see for the login, the response model set to
a
schema token. So let's import token inside our test users.py. So here schemas, we already
have
access to schemas. And so I can say token, or what should I call this? I'll just call this
login.
Res, which equals and then we'll call in schemas dot. I think it was a token. Was it token or
token
data? I think it was token. Well, let's just double check. It's token perfect. Okay, so
token,
and then what we're going to do is we're going to spread res dot JSON. All right, so that's
going
to perform a little bit of validation. Right. And then when we get a token, I want to
actually
validate it. So how exactly do we validate it? Well, we want to decode the token. And so all
of
the logic that I'm going to implement here is essentially the logic that we use within our
OAuth 2.py file. So when we want to get the user, we want to verify the access token is valid.
So
how do we do that? The all of the logic is right here. So we're going to decode the token. And
we're going to make sure that the user ID is within that token. And that's all I'm really going to do
at
that point. So I will copy this. And we have to import JWT from. So from, I don't know if
it's
Jose or Josie, who knows. So import JWT. And what we'll decode is going to be in the login
underscore
res under the access token property. And then we have to pass in the secret key as well as
the
algorithm. So the secret key can be accessed with settings dot. Well, I have to import my big.
So
we'll say from app dot config, import settings. And now we have access to our environment
variable.
So I'll do settings dot secret key. And then the same thing goes for the algorithm. So I'll
set
this to settings dot algorithm. So we're able to de load it, decode it. And then I'll remove
this
check, I'll just say, ID equals payload dot get user ID. Alright, and so once we get the user
ID
from the token, what we can do now is I can just assert does ID equal equal. And then is it
going
to match the ID of test user. So if we take a look at what test user was that's coming from
this picture. And so it's going to return Oh, it looks like I did not know no, it should be on there.
So
since we grabbed the response, the ID should be there. So we can just call this test
underscore
user. And then we can just call ID. So I think that's a little bit more of a valid
check,
because it's actually decoding the token just to make sure we didn't break anything there. And
one more thing we can assert login underscore res dot token type. So this should be set to
bear.
Alright, so let's test that out now. And everything worked. I think I've got
one
random print statement that I don't want. So I'll just remove that. And we have now
successfully completed our login user test. Okay, guys, so before we actually create any more new
tests,
what I want to do is I want to create a special file under our test directory, called conf
test.py. So this is a special file that py test uses. And it allows us to define
fixtures in here. And any fixtures you define in this file will automatically be accessible
to
any of our tests within this package. So it's package specific. So anything within the test
package, even sub packages will automatically have access to any of these fixtures. The reason I
want
to do this is I want to take all of our database code essentially, or well, our database
fixtures, and I want to copy all of it. And I want to move it to Conf test. And so by moving it to Conf
test,
this fixture, the session fixture, as well as the client fixture, will all be accessible to,
you know, test calculations, test users, any other test files within the test folder,
automatically. And so now, I don't actually have to import anything. So I don't need to import
from
Conf test, it'll automatically do that, it'll automatically have access to client. And
it
automatically have access to session. And what I'm actually going to do is I'm going to move
this test user to Conf test as well. And so that way, because I imagine that even for other modules that
I
create, you know, especially like for voting, and a few and creating posts, they're going to
need test
users as well. So it makes sense to put this in this contest. So everyone has access to it.
And I don't have to go around constantly importing things around like that. And now within our
test
users, I should be able to leave everything else. And you'll see that we're not importing
client or
test user anywhere here. But when I run this, we should see no issues. So let's give this a
try.
And you can see all of our tests passed with absolutely no issues, because we are importing
those from the contest file. And keep in mind, you know, let's say I created a new
package,
I'll call this whatever about API or something, it doesn't really matter what I call it. And
I've
got a whole bunch of tests in here, you know, test underscore something that p y. Because
this
package is still within the test package, it'll have access to all of the mud, all of the
fixtures
in the contest p y. But within a package, I can also create a new contest. So I can make a
new
contest.py down here as well. And when I do it within this package, then test users doesn't
have
access to that one. But anything within the API package will have access to the fixtures and
in
the API folder as well. So contest is package specific. So every directory you create, you
can
create a separate contest file, so that you can essentially scope fixtures, or you can make it
so
that fixtures are automatically imported into certain parts of your tests, and not others. But
we'll delete that because I don't actually need that. Alright, so we finished our
successful
login user test. The next thing we want to test is for a failed login. So I'll do the same
thing,
I'll call this about test incorrect login. And once again, a few things need to happen.
Let's
we want the test user so that a test user gets created. So we can try to log in with his
credentials, or at least with the wrong credentials this time. And we do need access
to
client just because we're going to be making a request. And here I'll say res equals client
dot
post. And we'll say login. And for the login data, we'll say data equals and then we'll do
a
dictionary here the username, we can use, in this case, test underscore user, email. And then
for
the password, we can just I mean, we could just give it any arbitrary password, we'll just
say
wrong password. So we know that's the wrong password. And so when a user enters in the
wrong
credentials, we should get a 403. So we'll say assert res dot status code equals equals
403.
And in the response, it should tell us that we have invalid credentials. So I'll say
assert
response dot JSON, so we'll convert it to JSON, we will get the detail property. And that
should
equal invalid credentials, make sure you get the capitalization correct, because it's going to
do
an exact comparison. And just to confirm if that's what the capitalization looks like, it
should be
invalid credentials. So just copy it directly from here. And this should be res not
response.
Let's try this out. All right, and the incorrect login worked. Perfect. So that means we did
get a
receipt, we did receive a 403. And we did receive the message invalid credentials. But not
only do
I want to test the wrong password, I also want to test wrong email as well. And I also want to
test
wrong email and wrong password and a few different things. So how do we do that? Well, we can
use
parameterize, right? So let's import pytest, which we already have. And we'll define our
decorator.
So pytest dot mark dot parameterize. So what are the properties that we want to pass, we're
going to
give it the attempted email, the attempted password. And then we can use
parameterize.
And then we could also get the status code. And we could also, you know, match on a
specific
detailed message if you have a different detailed message for wrong password, wrong email, but
we don't. So I'm not going to include that. And then now we'll just give an array with all about
why
does it do that. And so we'll here we'll define our tuples. And I'm going to copy and paste
this part just because I don't want to have to make you guys sit through this. But I've got a couple
of
different test scenarios. So here I've got the email being wrong email at gmail.com with
the
correct password, we expect to 403. I've got the right email wrong password, I've got the
wrong
email, wrong password. I've also got no password, sorry, no email. So if you put none, this
is
essentially leaving in blank. So with this set to none, you'll see that our API actually will
send
a 422 because the schema validation fails because it expects a username or password. And then
we'll
try sending it with the correct email, but no password at all, we should get a 422 in this
case, because pedantic performance validation and found that it was missing fields.
So that's what I'm doing here. And then we're just going to update all of this. So the
email,
we yeah, the email no longer going to be test user. It's going to be
well, actually, we fresh all we have to pass it into our function. So email, password, and
status
code. So we'll say email, password is going to be password. And status code is going to be
status
code. And we'll get rid of this check. Just because the invalid credentials won't apply when
we get
the 422. Let's try this out now. And we can see all of our tests have successfully passed. So
we
have now finally set up a test for making sure that when a user does not provide the proper
credentials, we get the right response code and the right data back. And keep in mind guys,
right,
in this case, I just set this up to be a very simple test just to check on the status code.
But you could check any single field that you want. It's up to you, you can make your tests
as
specific as possible, you can make them a little bit more generic, it's up to you to decide on
what you want to validate yourself. And like I said, I'm trying to keep this as basic as
possible,
try to just make sure that we focus on how to actually create tests, and less on the actual
specific tests. That's all of the things that I want to do for all of the user tests. Let's
move
on to all of the post tests. So tests when it comes to creating reading, updating and
deleting
posts. So I'm going to store this in a separate module just to keep things nice and organized.
However, you can keep everything in one file if you really wanted to, but
no reason to do that. So I'll call this test underscore post p y.
Now, when it comes to working with posts, we do have to deal with a little bit of an
extra
challenge. And that is, if you take a look at all of our posts, path operations, they
all
require authentication. So how exactly do we deal with authentication? When it comes to
testing?
Well, you're probably thinking, you know, we could, you know, define a post, define a
test,
and we can call this, you know, get all posts or something, right? And then here, we can pass
in
client. And then maybe test user. So we have access to that. And then we would just do, you
know,
client dot post, login. So we log in, we get a token. And then after we get the token, then
we
can then we can make a request to post. And that is technically valid. But what I would like
to do
is I'd write I'd rather set up a fixture that automatically does this for us. But instead
of,
but instead of actually making a request to our API, I want to just import the the
method,
the OAuth 2 method for creating an access token. So we can just import this into our test so
that
we can actually fake our own token or create our own token without having to use the entire
API
and go all the way through back into this same exact method to get a token because that seems
like a waste. And we're testing a whole bunch of other things that we don't want to test in
this
specific test. So what we're going to do is we're going to go to comp test. And I'm going to
create
a new fixture. I'm going to grab this. And I'm going to call this token. So we'll say
token,
and this is going to be dependent on test user. So we need a user to get created first before
we run this fixture. And what we're going to do is we're going to import this specific
function right here, create access token from OAuth 2.
And so I'll go from app dot OAuth to import, create access token.
And just to quickly recap on how that function works, we pass in a dictionary. But I don't
actually know what is in the dictionary. So let's go to where is this? Who calls this?
I think it's no, it should be under users. Yeah. So create user. No, it should be under login.
So
under login, this is where we create the token. So we pass in data, which is just a property
called
user ID, and then we pass in the ID. That's all pretty straightforward. So we can do
that.
So we'll call create access token. And it's going to be a dictionary. And we call
this
was a user underscore ID. And we're going to pass in. And we can get the ID of the user from
test underscore user, which is being passed in and we'll grab the ID property.
And we'll just return this. So this is going to return a token. And that's why we call
this
function token. Then what I'm going to do is I'm going to create a another fixture. I know
there's a lot of fixtures here. But you'll understand why I want to do this. So I'll do def. And here
I'm
going to call this authorized underscore client. The reason I'm calling that is, if we take a
look
at our current client, this just gives us an unauthenticated client, this new fixture will
give
us a authenticated client. So anytime we want to deal with any path operations that
require
authentication, we can just call this client the authorized client instead of the regular
client. So that's going to save us a lot of time. And this is going to require the regular
client
and token because we need to get a token. Alright, so we have access to the original client.
And what
we can do is we can update the headers, because we need to pass the token inside the header.
So we do client dot headers. And then we have to pass in a dictionary. And what we want to do is we want
to
spread out all of the current headers. So we do client dot headers. And then we want to add
one more, which is going to be the authorization. And this is going to be an f string, because we
need
to add the word bearer before, and then we can pass in token. And then we'll return this
client.
So it's basically taking the original client, and then just adding this specific header that
we get from the token fixture. That's all. And so now when we go into get all posts,
and I forgot to put the word test before it. Instead of client, we want to import authorized
client.
We don't necessarily need test user. At this point, it'll automatically get called because,
you know,
this calls token token calls test user, we only need to pass in test user if we want access
to
specific user data, but we're not using any of the information at the moment. So I'll do
authorized client dot get is going to be slash posts. We'll set the response equal to
that.
And we'll just do a print res dot JSON just to see what that looks like.
And I believe the status code should be a 200 because we're retrieving data. So I'll do res
dot status code equals equals 200. Let's just take a look and see what exactly is happening
here.
And now it's going to be test posts.
And we get an error. So let's see what happened. Authorized client not found. So let's I think
I forgot to save. Let's try this again.
And I forgot to turn this into a fixture. So right now it's just a regular function.
So
let's make sure I add the fixture there. So it's accessible to everyone.
And now if I run this, okay, so one passed, let's take a look at the data that we got
back.
So when I did a print, print of res dot JSON to see the data, I got an empty array. And the
reason
we got an empty array is that there's no test data in our database, there's no test
posts,
genome. So we're going to have to actually create some test posts before we run this test that
we can actually verify that we got what we expected to get. So just like we did with the login
user,
where we created a test user before, we're going to have to do this with our posts, we need to
create a whole bunch of posts, and then verify that we are able to retrieve those posts. So
we'll
tackle that in the next video. So let's create a fixture that will create a few initial posts
so
that we can actually test retrieving all posts. And we can use that same fixture for
performing
a few other tests or things like updating a post, right, we would need to have a post in our
database to update a post, same thing with deleting anything with getting an individual
post.
So this fixture is definitely going to be very reusable across a lot of our tests. So let's
figure out where we want to store our fixture, we could keep it in our test that posts
test underscore post file, because all of our posts, all of our test posts, sorry,
our
tests for post data, I guess, if you want to call it that will require that fixture probably.
However, there are going to be other tests in our application that will need posts initially. So
when
you want to vote, we need a post already in our database. So I'm actually going to put it in
our conf test file so that you know, everyone has access to it. Well, technically, anyone can
access
any fixture if they import it, but having it in the confidence file makes it simpler because
we don't have to import it. So I'm going to put it in here. And I'm going to this, this fixture
is
going to be a little bit different, just because we're actually going to create the posts in
the database ourselves. So here, I'm going to call this test underscore posts. And so what we need
is
a couple of things. So first of all, we need a test user, because if we don't have a user, we
can't create posts, because all posts need to be associated with the user. Since we're
going
to work with the database, we're going to pass in session also. All right, and let's define
some
post data. So I'm going to create an array of dictionaries. And I'm actually going to copy
and
paste some data just because I don't want you guys to sit and watch as usual. All right, so
what
we're doing here now is we're creating three posts. So in this post data, I have an array, or
sorry, in a list, and then a dictionary. And then here, we're providing the title,
and the content, and the owner ID. Alright, so that's going to create a post. And
we're
grabbing the owner ID from the test user fixture. And we're just gonna grab the ID from that.
So we're saying the owner ID, and then I'm just going to create another post here and another post
here,
for the same user is essentially creating three posts. And so now, since we have access to
session,
we can actually just directly tap into our database and create that information for us, or
those posts. So we can do well, the when it comes to SQL alchemy,
if you want to add multiple entries into a database, we have access to the add all
method,
so we can call session dot add underscore all. And then here, we have to pass in a
list
of user models. So we would this would be, you know, schemas, but not schemas, we'd actually
have to import models. So I'm going to import, do I have models
already in here doesn't look like it. So I'll import that as well. So we'll say from app
dot,
let's just say from app, import models. And I can say models dot user. So we'll create a new
user
model. And then we have to pass in all of this data. So right, we would need to be title
equals, you know, blah, and then then content equals blah. And then we'd have to do another one. So
I'd
copy this, paste this in here. And so on, and then this would add it all at once. Now I can
manually
hard code this value into here, just to kind of keep things simple. And I could do that. And
actually,
I will show you how to do that just just in case, but I'll show you what I did when I was
doing a dry run. So I'll copy the title is going to be set to first title. The second title is going to
be
here content and second content. And then the owner ID is going to be the same for both
luckily,
well, all three actually. And that's not in the right format.
Oh, don't do that. It should be equals. And then the same thing here.
And if we wanted to, we can copy this last one and then put the third one
in.
This will be called third title. Third content. All right, let's save this. The auto
formatter
did not do much to help me out here still looks pretty ugly, but that's okay. And so that's
one
way of doing this. So it'll add it all. And then all we have to do is just do a session dot
commit.
And then to get all the data back, I can just do a session dot query. And we'll just say
models dot post. Oh, this should be user, by the way, not
I mean, this should be post not user, because we're creating a post and not a user with
a
models dot post. And we can just grab all of them. Now, if you wanted to be able to take
this
dictionary and convert it easily to this format, there's a couple things you have to do.
But
technically, you could just leave like this and it'll work fine. But just this is a little bit
more of Python than anything else. If you didn't want to hard code it in like this,
because at this point, we don't even need this dictionary. But if you wanted to take this
dictionary and convert it into a list of user models or sorry, post models, what we can do
is
we can use the map function. And so the map function works like this, you call
map,
then you call some function that you define yourself. And then you call the posts
underscore
data list that we have up here. And so this function, what it's going to do is it's going to
iterate through this list. And it's going to take each item in the list, which is a
dictionary,
and it's going to convert it into a model dot post. So how exactly does that look?
Well,
let's first of all, let's rename this. And I'm just gonna call this maybe create user model.
And
I'll define a function up here. And I don't know why I keep saying user it's a
post,
reading posts, not users, create post underscore model.
And this is going to get passed in some, some, some object, but technically, what's going to
get passed into this function is each dictionary. So I'll call this post underscore
data.
Well, actually, don't call post data, I'll just call this post. And so we have to put the
logic of converting a dictionary
into a post model. And that's pretty easy. All we have to do is just say, models dot
post.
And then we take post, and then we spread it. That's it. Alright, so what's happening here
is
we're taking the dictionary, which is what it's going to get passed into here. And then
we're
taking it and spreading it so it looks like title equals blah, content equals blah. And then
we can just return that. And then this map, we just set it equal to some variable, I'll call this
maybe
post map. And I'm forgetting an E. Okay, so then, so that's going to return. So that's going
to,
so that's going to essentially convert all of those two user models, but it doesn't return a
list, it returns a map, which isn't quite the same thing. So to convert it to a list, all we have
to
do is then say, posts, equals, we just say list, and then post underscore map. So I'll convert
it
to a list. And then under a session dot at all, instead of hard coding those values, I can
just say session dot add underscore all. And I just pass in posts. And we can just comment out
this
line right here. Either one works, choose whichever one you like, I don't really care. But
this is all
we need to do, we just have to return this. I'll save this as posts. And we'll return
posts.
And that should create three posts in our database. And so now if I go to test
posts,
and make this reliant on what did I call it get test posts. If I then run this, you can see
that
we did in fact get multiple test posts. And it's up to you to decide what checks you want to
perform.
If we go to our post route, and we take a look at the schemas, you can see that it's going to
be a
type list of schemas that post out a list comes from the typing library. So let's see if we
can
actually do this. I never actually tried this with this. But I think it should work. So I will
import
that. And then we'll also import from app from app import schemas. And I'll just
say
posts equals res dot JSON. And I'll say actually schemas dot what is it post out? Yep, it's
going to
be post out. I don't think that'll work because yeah, I don't think that'll work because
we're
this is expecting one individual post, but this is going to be a list of posts. So I don't
actually,
I wonder if I can do, yeah, we'd have to do this manually, like we'd have to iterate through
it and
then basically copy it out. And I think that would just take too much work. So we'll leave
this
section out. If you guys want to take that as a challenge, you can essentially create a post
pedantic model for each entry within within the response and then try to validate each single
one.
So instead, I'm just going to do a simple check, I'm just going to say assert. And we'll just
grab the length of response dot JSON. And I'm going to set this equal equal to length of test
underscore
posts, just to make sure that we get three exact posts. And if we ever change the number of
posts
that we have, this should automatically get updated. I think that's a good enough check for
this test. Let's try this again. And we shouldn't get any errors and it looks like it passed
just
fine. Okay, so I figured, you know what, let me go ahead and just show you how we can
actually
do the validation using our schemas, we're going to use the same exact logic that we use here
when it comes to performing the map, because we're essentially going to take a list of
dictionaries
and convert it to a list of schema models. So we're going to go back to our post our
test.
And what we're going to do is we're going to first define a function that's called, I don't
know, we can call it validate. And this is going to receive a post.
And that's going to be a post that's going to be a dictionary that we get back. And all we're
going
to do is we're going to return schemas dot post out. And here, we're going to pass in the
data,
we're just going to unpack it with post. So that's all we have to do. And then we can call the
map function like we did before. And I'll store this in a variable called a post posts underscore
map,
which equals map, we're going to call the validate function, and we're going to pass in res
dot JSON.
And so we have a map, if you wanted to, you could technically convert it to a list by calling
list post dot map, but we're not doing anything with that data.
However, just to show you, we can just print this out real quick. And so now if I run
this,
and we take a look at the print statement, you can see, we've got a post out model. And it
does all
the validation. So it's going to make sure all of the properties are set and are there. And
because we have it set as a post, as a post out pedantic model, you'll see that it's very easy to
perform
extra checks with the cert statement. So now I can do other checks, like maybe I want to
say,
actually, let me first save this as a variable say posts list equals list of posts map.
Oh,
and that's supposed to be post map. We have access to this list. And now what I can do is I
can just
say, assert. And here we can grab posts underscore list, grab the first post. And we can check
certain
fields, I could say like, Hey, does the ID sorry, it's gonna be the post dot ID. Does that
equal
equal test underscore posts? We'll grab the first object, and then grab the ID. Well,
actually,
it's going to be, I think I could do just dot ID. Yeah, because test post, keep in mind, when
we do
test post, this is going to return this is going to return a not a pedantic model or a
dictionary,
but this is going to return a SQL alchemy model. So a little bit different. But we just call
it dot ID. And I think this should work. So let's test this out. I did not work. What happened
here?
Yep. So I accidentally grabbed. I see. Yeah, that so this is expected just because I don't
know the
order that we're going to retrieve the posts in. I didn't provide a order by filter criteria
when
it comes to my my get post functionality. So it could be returning any posts right now it's
saying
that a this ID happens to be to this one happens to be one. So it the order doesn't matter. So
this
isn't exactly the best way to do it. But if you want to figure out a way to set up the
order,
so that you always receive maybe the post with the lowest ID, and then you compare the two,
that would be perfectly fine. And then you can check other properties like what's the
the title? Does it match up with what we should expect it to be? Does the content match up
with the content, you can put as many asserts as you can. But by having by doing this, we are able
to
perform a validation to ensure that at least we get what we expect, all the fields are there.
And then it's just up to us to set up the necessary search to verify that the values are
correct.
But I'll remove that. And I think it's okay, just to keep it like this for now. All right, so
we set up a test for getting all posts. And what I want to do now is I want to test to
make
sure that a unauthenticated user is not able to retrieve all the posts. So I'll create a new
test
and I'll call this test. How about unauthorized user underscore get underscore all underscore
posts.
And instead of using authorized client, because we want to test if a unauthenticated user
tries to
access as they get a 401, we'll just use the regular client, which is going to be a non logged
in user. And we want to make sure that test posts are here as well. And remember, because
we're
calling test posts, that's going to ensure that because test post is reliant on test user,
it
will in fact be a test user as well. So just to give you a heads up, we don't actually have to
pass in test user into here, because this should handle that automatically. And here I'll just
say
res equals client dot get. And we'll just go to posts again. And I'm just going to assert a
simple
401. So res dot status code equals equals 401. That's really the only check I want to do
there.
If they're unauthorized or unauthorized, there's no other things to check really. And we can
see that it did successfully pass. Okay, so we've tested the get all posts, we're
going to now move on to getting an individual post by an ID. But before we do that, I
think
it's best to actually test getting an individual post by ID when you're an unauthenticated
user,
because we can essentially just copy this code. And I can just paste it here. And instead of
doing a user get all posts, this will be user underscore get one post. And so everything
else
can be the same, we just have to change the specific URL. And so in this case, we're
just
going to grab the ID of one of the posts. So here, I'm going to change this to an F string.
And I'm going to say test underscore posts. I will grab the first one. And then
remember,
this is a SQL alchemy model. And so we actually have to convert it to a dictionary. So we have
to do dot underscore underscore dict underscore underscore. Sorry, the test post is not a
SQL
is it a secret? I think I could do ID. I think that should work. Actually, nevermind. I think
we should be able to do that. That should be fine. And this should be a 401. So let's test this
out.
Let's make sure that this works. And that works. And what I also want to do is I want to test
for
a post test to retrieve a post with an ID that doesn't exist, I guess. So I'll call this a
def
test. Get one post, how about not exist. So this one, we're going to need an authorized
client.
And then test underscore posts as well. And I'm going to copy this. But we're going
to
change this and I'm going to hard code some random number like 88888. And then we'll
just
assert res dot status underscore code equals equals 404. Alright, so now we're going to
try
to retrieve a post that doesn't exist. And it looks like it passed perfect. So now let's
actually
try to retrieve a valid post. This one's gonna be a little bit more work. So we'll say def
test, get one post. And we're going to copy these two. And I'm going to copy this one right here.
We're
going to retrieve the first post from test posts with an ID of zero, I think. And I'm just
going
to rename this to authorized client. And let's just do a print, let's see what this looks
like.
So we'll say res dot JSON. All right, and that's going to run. And it printed out the post. So
we
can see what that looks like. And just like we did before, we can also do a validation. So I
can say
post equals schema schema dot what is it? I think it's post out. Yeah, post out. And we're
going to
pass in res dot JSON. And do we need to unpack it? I think we have to unpack it. Let me let
me
print out post just to see what that looks like. Actually, I could just take a look at what we
did up here. Yeah, we have done back it or spread it whatever it's called. Yep. So now we've got
our
post model. Well, it's our pedantic model. And so that does some level of validation. And then
at
this point, what we can do is I can say assert, and I can say does post dot and then take a
look
at the format, we have to grab the the post property. So it's this first one. Yeah.
Yeah,
so we'll grab post dot post dot ID equals equals, then we're going to compare it to the
ID
from test underscore posts. So it's a test underscore posts, zero dot ID. And I think
that
should still work. Alright, that works. And we can check a few other things. So we can say
post
dot post dot content. And if you're wondering why it's post dot post, remember, if you
actually take
a look at our schemas, our post out, you can see that we have to grab the information about
the
post, we have to go into the post property. And then from in there, we can then take a look at
all of these fields. So that's why we have to do it like that. And we'll say test underscore
posts,
zero, and then let's see if we can do content, I believe this will actually throw an error.
Because this is a SQL alchemy model. So it's going to, oh, that actually did work. I'm
surprised.
Okay. Alright, that worked. So if you guys want to, you can also essentially copy this line
and
then check to see if the titles match. You can essentially check every property if you
really
want to. So we'll just run that one last time. Perfect. And I'm just gonna remove this
print
statement. And so those are all of the tests that I want to do when it comes to retrieving an
individual post, I think. I'm sure there's a couple of tests you guys might be able to think of.
So
feel free to implement that. But I think that's a good starting point on this. But in the next
video, we'll take a look at creating a post. Okay, so let's move on to creating a
post,
I'll create a function, we'll call this test, create post. And we're going to need an
authorized
client. We're going to need a test user for sure. And lastly, this is purely optional. If you
want
to, we could also set up all of our test posts. If you wanted some posts in the database
before you created one just to potentially see if you run into any issues when it comes to creating
posts,
when there's already posts, but it's optional. And we're going to set the res and we'll set
this
to authorized client dot post. And the URL is going to be slash posts. And then let's give
all
of the data for our new post. So we'll say JSON equals title. And you know what, instead of
doing
this, let's go ahead and set up a pytest dot what was it called? I already forgot what it's
what
did we call it a parameter. So Mark parameterized so that we can test multiple different
values.
Mark dot why is that not working? Okay, I have to import pytest.
We don't need this line right here. So we'll say import pytest. And so what are the values
that we need? How about title? We need title for sure. We need content.
And then we can also give a published value if we wanted to. And then let's give all of our
come
on VSCR. Don't do that. Alright, fresh tuple. What do we want the title? How about awesome new
title?
Oh, awesome new title. What's the content? Awesome new content. And what do we want published
to be?
How about true? And we're going to copy this and I'm just going to put in some new
values.
Favorite pizza. And we'll say I love pepperoni. Oh, published. Let's say that's still a work
in
progress. I'll list skyscrapers. Alright, so we've got some test data. Let's pass those values
in.
So title is going to be set to title content is going to be set to
content.
And then finally, published will be set to published.
Alright, we get a response back, let's do a little bit of validation with our pedantic model.
So I can say schemas dot post. And we'll just take the response,
convert it to JSON, of course, and set that to create it underscore
post.
And then we can just run a couple of checks. So let's assert the status
code.
status code. So for creating a new post, that should be a 201.
And then for we can just assert a few extra values. So I could say like res
dot
actually, I can grab created underscore post dot title should equal title. And I can just do
that.
I need two equals. And then I can just copy this a few times will change this content content,
and then published.
And while we're at it, we can also verify that the owner ID gets set. So I can say created
underscore post dot owner underscore ID equals equals and then we can grab a test user
ID. So I think that's a good number of things to check. So at this
point,
I think it's safe to say that all of these should work. And all of those past awesome. So
we've got the create post done. There's one other thing that
I want to test with create post because if you go back to our, where does it our schemas?
Where's
our schema? I don't know if we have a create post post create. So that's just going to be from
post
base, we can see published is set to a default value of true. So let's test to see if the
default
value of true gets set. And we theoretically could have put this test in here just by doing a
none
here. But I like to separate this out into a different test because we are testing a different
functionality in this case. So we'll say test, create post default, published, true.
I'll do authorized underscore client and test user. Once again, we don't need test posts.
But
if you want to have it in the database, just to make sure that everything's not broken when
you already have posts already in there, we can keep that in.
And I'm just going to copy this. Actually, I could essentially copy all of
this.
And we can just give it arbitrary titles and then published will leave out published. We don't
want
that anymore. And I'll just copy these values here. And published should be set to true
by
default. So even though we're not passing it, it should get set. Everything else should still
be just fine. So let's test that out. Oh, it looks like it failed. So what happened
here?
Oh, I just pasted in the wrong values.
All right, and then one last thing, just like we did for all the other routes within the post,
we also want to test if we're not logged in. So I'm going to copy this one
and just rename it. So test unauthorized user
create post, we don't need to have test user in there. But why not?
And I'm going to just copy this right here. And then just change this to a plain client. All
right, and this should work.
All right, and that passed as well. So we have got all of the create post tests done. I think
in the next video, we will tackle either deleting or updating a post. Okay,
so let's move on to deleting posts. And the first thing that we're going to do is we're going
to test a unauthorized user trying to delete a post. And we'll say test unauthorized
whoops,
delete post. And we'll have client test underscore user. And we'll definitely need test posts
in this
case. And then we can essentially copy well, copy this one right up here. And we'll change
this to
we don't need the body anymore. And the route is going to be we're going to pass test
underscore
posts zero dot ID. All right, let's test that out.
Yep, and I realized this should be a delete actually not a post. That's why I got some
other
status code. Perfect. Alright, so we got that one to pass. And so now let's test a valid
deletion.
So I'll say test, delete post. And ideally, I guess we should be calling like
success,
like we successfully deleted it. And this time, we'll get the authorized underscore
client.
We'll get the test user and the test underscore posts. All right, we'll call this same request
here
and just change this to authorized client. And this should give us a 204. I mean, you
theoretically could also, you know, then fetch
all posts again, just to verify that the total number of posts is one less. But we're not
going
to do that in this case, you could however, do that. And that failed. So what happened
here,
a 422. And two things. So I don't even know how this one ran with the same issue, we
should,
we need to make that an f string. So I genuinely don't know why that actually worked. A little
alarming, but let's test both of those out.
All right, all of them passed. So we've got that done. Let's test deleting a non existent
post. So an ID that doesn't exist in our database,
I'll say test delete post non exist. And I'll just copy these. And I'll copy this.
Actually,
I'll just copy all of this. And I'll just change this to be some ridiculously high number.
And
this should be a 404. All right, that one looks good. Alright, so the next one's going to be
a
little bit trickier. We're going to create a test where a user tries to delete a post that
isn't
theirs. They try to delete a post that's owned by someone else. So we'll say test, delete
other
user post. Now there's a couple of issues here. So first of all, we need to actually have
more
than one user in the database. And we need to have posts owned by multiple users now. So
this
creates a couple of challenges. So how exactly are we going to set up more than one user?
Well,
I think it's best to use fixtures like we always have. And ideally, I would have set up a
test
user. Where is it my test user, I would have set up a test user to actually create multiple
users
in our database, but I didn't. So I just have one. If we had set it up to the multiple user in
the
database, then this test would be a little bit easier. Since I didn't, what I'm going to do is
I'm essentially going to copy this, I'm going to copy this. And right above it, I'm going to
create
just a second one. I know it's not very original, probably not the most efficient way of doing
things. And it's gonna be dependent on client. And we're just going to put a one or
something,
it doesn't really matter. Just so that we have another user, I'll make this 123. And so now we
have two users, which is perfect. And down here in my posts data, what I'm actually going to do
is
I'm going to create one extra post. And we could just copy this data right here. And here,
I'm
actually going to pass in an extra picture, which is going to be test underscore user two. So
before
test post gets created, we're going to create the second user. And here, I could just grab the
idea
of test user two. And actually, I don't even need to do that. I don't know why I did that.
Let's, we don't even need to create a post owned by that user. We'll leave it like this for
now.
Nope, I was right. Actually, we do. I don't know why I'm flip flopping like this. So, so the
fourth post 1234 is going to be owned by test user two. And so now if I go back to
test
posts, here, we're going to be logged in, remember, authorized client is always going to be
logged in as user one. So we're going to try to delete that fourth post. And so we can do that by
grabbing
the ID of the fourth post. So we'll say test underscore curly braces, test underscore
posts.
We'll grab the fourth one, which is an index of three. And then we'll grab ID. And this
should,
I forget what kind of error it should get or what status code. I think it's 403. So we'll do
assert res dot status, underscore code equals equals 403.
And it looks like that's successfully passed. And so that one's good. And that pretty
much
wraps up all of the delete tests that I want. And then in the next video, we'll take a look at
updating posts. Alright, so let's update a post also a def test underscore update
post.
Here, we need an authorized client. And we'll need a test user. And we'll need test underscore
posts.
Alright, and I'm going to define a dictionary with the values that I want to update. And so
here, I've got the title, the content, and then the ID, which I'm grabbing from test posts. And I'm
just
going to use the first test post in there. And I'll say res equals authorized client dot I
think
it's put. And then we're going to do an f string. And we'll say slash posts. And this one will
be
test underscore posts zero dot ID. And then we have to pass in the data. So we'll do JSON
equals
data. So we're passing in this dictionary. Alright, and just like we do with the other routes,
if we go to the update post,
technically, we're sending it out with the schema of posts. So we can just quickly validate it
on our end on the test as well. So I can say updated post equals schemas dot post. And
we'll
pass in res dot JSON. And then we can assert a couple things. So we'll say res dot status
code
is going to equal what's an update, I guess we use 200 in that case. And then we can
assert
a few things. So we want to make sure that the new title we can do updated underscore post
dot
title equals equals data dot updated. Sorry, data dot. This is a dictionary so title. And we
can do
the same thing for the content. And I think that should be everything that we need. And just
to,
you know, just to make sure that my tests don't take too long, I'm going to start commenting
out all of my other code, all of my other tests in this file, just to make things a little bit
quicker.
But you can see how it starts to slow down on just with just a couple of tests, really.
So
we can see that it did successfully pass. So that's good. We've got the first update post
test.
The next thing I want to do is I want to try to update another user's post. So we'll say
update other user post. So this we need authorized client. We need test user. And we
need test user to and test underscore posts. So we're going to try to update that last
post.
And so I'm going to define a dictionary with the values I want to update it with. So same
thing, title, content, the only difference is the ID is going to be, I think it's
actually
three, the ID is going to be of the third, sorry, the fourth item in that list. And I just
want to
make sure it's the fourth one. So 1234. So yeah, we need a value of three. And I'm going to
copy this
and just change this to three. And we should get a status code of 403. And let's test that
out.
Perfect. And the last thing we need to do is a unauthenticated user trying to update a
post.
Actually, there's two more tests. So let me grab unauthorized user, this one right
here.
And this is going to be a put. And that should return a 401. And the last thing that we
need
is, and actually, I need to update this name. So unauthorized test update post. We can also
set
up a test to update a post that doesn't exist. And I believe we can just copy the deleting
the
post that doesn't exist delete post delete post non exist. So I could just copy this one.
And
change this to a put. And we'll change the name to be update. And let's test this out.
Hopefully
everything works. Oh, one failed what happened here. Okay, so it tries to validate it
because
I didn't provide any data. So I can just copy this data right here, just so we can give it
something.
And so we can say, JSON equals data. That's going to prevent us from getting validation
errors.
And that passed perfect. So let's take all that I'm just going to uncomment everything else.
We can get all of our tests back. And that's going to wrap up updating posts. And
that's
actually going to wrap up all of my post cases. And so I guess the last thing that we got to
do
is set up all of the voting tests, then there's only going to be a couple of voting tests, but
it should be fairly quick. Alright, so let's move on to setting up our test for
voting,
I'm going to create a new file, we'll call this test underscore votes.py. And let's define
our
first function. So I'll call this a test vote on post. So this is going to be a successful
vote
on a post. What do we need? We need an authorized client, of course. And we need to make sure
that
there's some posts already in our database. Alright, and now I can do a authorized client
dot
post. And here, this is going to be the vote endpoint. And let's just give it some data.
So
here, we're going to pass the post ID. So this will be post underscore ID. And we'll grab
the
first item in our list. Test posts dot ID. And I forgot my curly braces. That's why it's
giving
me an error right now. And we'll say what is the other direction. So we're going to be voting.
So
there's going to be a dir set to one. Alright, and now technically, if you look at what's
happening,
right, we're logged in as user one, and test posts. And if we go back to that fixture, where
is this guy that's owned by user one as well. So technically, we're voting on our own
post.
But if you look at our code, we didn't actually put any checks to stop us from liking one of
our own posts. So it's perfectly fine in our test to do that, just because our code is right
now
allowing it. If you didn't want to do it, then first of all, we'd have to update this code to
make sure that we perform a check to verify that, hey, are we voting on our own code? If
so,
sorry, voting on our own post, then we should reject it. But we don't have that
implemented.
So for our test, it's perfectly fine, because that's how our application works. But obviously,
if you wanted to, to make sure that you're testing someone else's post,
then you could just obviously pick the last item. So the third item in the
array,
because that's owned by test user two, you just would have to make sure that you pass in test
user two, in this case, just to make sure that he gets initialized. But that's a good
assignment for you guys, I would see if you guys can implement logic to make sure that you
can't vote on your own post. And then go ahead and create your very own test to make sure that
if
a user tries to vote on his own post, he gets an error of some kind. But for us, this is good
enough. I'll leave it as three just so we're voting on someone else's post. I'm okay with
that.
And we can just assert res dot status code equals equals 201. Don't really need to check
anything
else. And now I can just change this to test votes. Okay, so that one successfully
passed.
All right, the next thing I want to test on is what happens when a user tries to vote or like
a post that he's already liked. So we'll call this a def test underscore vote
underscore twice underscore post. And so we'll need an authorized client will definitely
test
posts. But there's one little issue with this one, right, we need to have a post that's
already
been voted on. And we don't have that right now. So how do we do that? Well, I mean,
theoretically,
we could initialize our test post fixture with some votes. But instead of doing that, I'm
going
to actually create a new fixture. And I'm going to keep it in this file, because all this
fixture will do is it will set up one of our posts to have a vote on it already. And I don't think
really
anybody else in any other test in our application is going to need that outside of the voting
file,
because that's the only one that's concerned with voting. So let's test that out. So we'll
import
py test. And we'll set up our fixture. So py test dot fixture. And I'll call this test vote
maybe.
And so for this picture to work, we definitely need an authorized client. Actually, we don't
need an authorized client. We'll need a we'll need test posts. We'll need a
session because we're going to just change it in the database directly. And then we'll need
our test user. And so let's, well, we're going to have to import our models. So I'm going to say
import
import from app import models. And I'm going to say models dot vote. So let's create a new
vote.
So we're gonna have to pass in the post underscore ID, which is going to equal test underscore
user.
We'll grab the third one, dot ID. And then we need to pass in the user
ID,
which equals Oh, it looks like we actually Yeah, we already have the test user. So I can say
test
underscore user ID. And then I can just do session dot add new vote. And I need to create a
variable
or store this in a variable. So new vote. And then we can do session dot commit. And so down
here,
I can pass in test underscore vote. And I'll say res equals authorized client dot
post.
And then JSON equals what is the post ID we'll grab this from test underscore
posts,
grab the third. And we'll grab the ID. And then we'll set the the dir to be a
one.
We're trying to re vote on that vote. And we should get a 409. So we'll do a cert res
dot
status code equals equals 409. All right, let's test that out. One error. Oh, man, what
happened
here? That's a key error. So something very simple happened. And I see my mistake, there
should be
test posts. All right, let's try that out now. And that worked. Perfect. All right, now
let's
try successfully deleting a vote. And so we will need all of these will also need a test
vote.
And so we do res equals authorized client dot post once again. I'm just gonna copy all of
this.
So we got that and then the direction is going to be zero for deletion. And then we just need
to
assert res dot status code. This is going to equal I think we just left it as a 201.
Okay,
so that worked. And then now we want to delete a vote that doesn't exist. So unlike a post
that
doesn't that we never liked. So we'll say def test underscore, delete, vote, non exist. So we
need
the same exact stuff right here. I'm just gonna copy all of this for now. And then we'll
change
up the few details. direction zero, same post. But I'll just grab. Actually, we don't want the
test
vote because we want to make sure that there isn't a vote. So we're going to remove that
fixture. And this should be a 404. Let's try that. That worked. And then finally, let's try voting
on
a post that doesn't exist. So yeah, liking a post that doesn't exist. So we'll say def
toast,
test, vote, post non exist. And copy all of these. I copy this that the direction to one and
the ID
is going to be 8000. And we expect the status code to be 404 as well. All right, it
looks
like everything's good. The last thing that we just want to check is make sure that a user who
isn't authenticated can't vote. So I will copy this test vote on authorized user. And then
we
want just the regular client in this case. We'll do test posts, three dot ID. And this should
be
was it a 401? I can't remember, let me go to the other one, it should be 401. Okay. All
right,
guys, so I think we've covered all of the tests that I wanted to cover. We don't need this
test
calculations p y file, but I'll keep it in there in the repo just so you can take a look at
it. The database.py file is absolutely useless now. So I'm actually going to just delete, I'll keep it
in
there just in case you want to see it. But most of it's moved to conflict test.py. But just
to
make sure that everything's still working, I'm going to run my tests for all Oh, what
happened
here? Nope, nope, nope, oh, oh. Okay, yeah, that was wild. All right, move this. Let's see
what
happens. Hopefully everything passes. And all 46 tests have successfully passed. So that
pretty
much wraps up all of the testing section. I recommend you guys think of different
scenarios
that could potentially break our application and set up tests for each and every individual
scenario,
because that's how you successfully set up tests, because you want to make sure you cover
every individual corner case that you can possibly think of. Currently, when it comes to adding in
new
features and making changes to our code, we have to go through a very manual and cumbersome
process
before we can get those changes pushed out to our production environment. And so what I think
would
be good is for us to set up a CI CD pipeline so that we can do all of this in an automated
fashion.
So I've created a couple of slides just to kind of go over what our CI CD pipeline is going to
do. And I've got the generic definition of CI CD. And it's probably not a very good definition. And
I
was thinking about just leaving out the slide, because it doesn't really provide much, but I
figured I just added just in case. So CI CD stands for continuous integration and continuous
delivery.
So continuous integration is the automated process to build package and test your application.
And
then the continuous delivery picks up where your continuous integration ends and automates the
whole
delivery of the application. So continuous delivery is responsible for actually pushing out
those
changes to your production network. So those two things make up our CI CD pipeline. But
let's
actually take a look at what our whole manual process looks like right now so that we can
see
where a lot of the extraneous time consumption and manual process takes place. So our
current
manual process is that, you know, we're going to make some changes to our code. And then
we're
going to commit those changes to get right because you know, we don't want to lose those
changes. And then after that, we want to go ahead and run py tests so that we can verify that our
changes
didn't actually break any known functionality to our code. And if our tests pass, we have
an
optional step of, you know, building any images, so things like Docker images, if we happen to
be
running Docker in our development and production environment, if we're not, then this is an
optional step. However, when it comes to the build phase, in this case, that some people refer
to
other programming languages might require you to kind of build the application at the end
to
actually be able to deploy it on Python, you don't really need to do that. So that's why it's
kind of an optional step here if you're not using Docker. But after we do that, we then move on to
deploying
our application. So I just have one row called deploy here. But keep in mind this, this
one
little block could represent multiple steps. Because depending on how you actually deploy your
application, making any changes or updating the code in your production environment
could
actually involve a very complex process. So with git or sorry, with Heroku, right, it's
obviously a
very simple process, because all we do is we just do a git push to Heroku. And that's going
to
automatically cause Heroku to kind of handle all of the the updating of the processes. So
that's a feature that's built into Heroku. But we only get that if we use Heroku, there's obviously a
number
of different ways that we can deploy our application. If we decide to go the route of
deploying our
application on an Ubuntu server, then the deployment process in this case would be us logging
into our
server, us pulling in the new code with the changes, and then restarting the service so that
our
application actually uses the new code. But if you're using, you know, some other
production
environment, or you use some other method to deploy your application, this could be an even
more complex process, something to keep in mind. So even though it's just one block listed here,
it
could actually be, you know, numerous steps. Alright, and so now I want to show you guys
what
our automated CICD pipeline is going to look like after we set it up. And so just like we did
in the
manual process, we're going to make changes to our code, obviously, that's a manual step,
because we have to implement the changes to our code. And then we're going to commit those changes. And with
a
CICD pipeline, or specifically with the one we're building, that's all the manual steps that
we have to do, we are done, we don't have to touch our keyboard or our mouse. It's basically hands
off
at this point, and we're going to let automation take over. And so usually when you commit
changes
to your code, that's going to trigger our CICD pipeline to run. And so what happens is
our
continuous integration phase starts at this point, as soon as we commit those changes. So in
the CI
phase, we do the first thing that we do is we pull our source code. So we pull our source code
so
that we can actually work with it. We then install any dependencies. So this is the equivalent
of installing all of the dependencies listed in our requirements that txt file, we then run
our
automated tests. And so that's running pytest. And then you know, assuming the test pass, we
then
build any images, this is once again, an optional step for us. It's only in that's only needed
if
we are have if we use Docker. But that wraps up the continuous integration phase. So once
that's
done, the continuous delivery phase is going to take over. And so the continuous delivery
phase,
what it's going to do is it's either going to grab the latest code, or the new build image,
depending on what our deployment model actually is. And with the new image or code, it's going
to
then have all the logic needed to push that new image or the new code into our production
and
make sure that our production is actually using our brand new code. And so these two steps are
really the only manual steps can be continuous integration in the continuous delivery phase
is
all done in an automated fashion, through code essentially. And so some of the more common CI
CD
tools include Jenkins, Travis CI, circle CI, and, and GitHub actions. Now for our
course,
we're actually going to use GitHub actions just because it's already integrated into GitHub,
it's free, we don't need to install anything on our local machine, there's no software that
we
have to set up because it's all hosted on GitHub. So it's like a hosted service. And it's just
going
to make things very clean, very simple, and it's free. So it's going to make and so everyone
will
be able to have access to it. So what exactly is a CI CD tool? What does it provide? And so
every
time I heard CI CD before I learned what it is, I always thought it was a very complex thing.
And
then when you find out what it actually is, it's actually this stupidly simple, it isn't
there's no magic behind CI CD. It's all very manual, even though it's an automated process, you'll see
that
when we actually go to configure it, it's not really doing anything special. It's doing
everything that we already do. But we're just providing instructions to a machine. So a CI CD
tool,
what it does is it provides us a runner. And so a runner is just a fancy term for a computer,
or a
virtual machine, it doesn't really matter, you know, what is the underlying infrastructure,
they give you a little machine that we can provide a bunch of commands for to run.
Alright, so just like in our terminal, where we run commands like pytest or, or pip install,
right,
we provide these commands to a computer. And these commands are usually configured or provided
in a
YAML or JSON file, or even through a GUI, depending on what your CI CD tool is. But the
different commands that we provide that little mini computer to run, make up all of the actions that
our
pipeline will perform. And this pipeline, or this virtual machine that we have is going to run
based
off of some event getting triggered. And so like I mentioned, this is usually us either doing
a get push or performing like a merge pull a merge request, it doesn't really matter. There's a
lot
of different events that you can configure depending on your CI CD tool, you can also set it
up to run every single night at two in the morning. There's a lot of different
options,
but mostly it's going to be based off of a get push. And I wanted to provide just a quick
example config of what our CI CD pipeline is going to look like when we're done. So this is just a
little
snapshot of it's not all of it. But I wanted to show you just how simple setting up a CI CD
pipeline is. So this is the YAML file that we use to kind of tell the machine, what are the steps that
we
want it to perform. And so you can see here, we've got this section called steps. So already,
you
can tell we're just providing it a list of steps. Now, the syntax may be a little foreign,
but
you'll see that it's pretty easy to learn. You'll see that the first step that we have is
there's this thing called action slash checkout. So what this is actually doing is it's telling our
machine
to pull our code. So after we do a get push, it's going to pull our brand new code so that we
can
actually do something with the code. And then the next step is here, we're setting up Python
3.9.
So all it's doing is it's installing Python 3.9 for us. And then afterwards, we have to
update
PIP. So it's going to run Python dash m pip install dash dash upgrade PIP. So that's going to
upgrade PIP. So we have the latest version, then we're going to install the dependency. So that's
PIP
install dash r requirements dot txt. So you can see all the things that we do on our local
machine, we're just telling this little machine to do the same thing. And then afterwards, we then
run
pytest. So that's going to first install pytest. And then we just run pytest. Right, just like
we did on our local machine, we're just telling the machine to do the same exact thing. So we're
just
telling the machine a list of instructions that we have done by ourselves manually, we're
just
giving that list to it so it can run automatically. And so hopefully you guys see just how
simple a CICD pipeline is, there's nothing magical to it. It's just like telling a machine, this is
what
we want to do. I want you to run this every single time we do a git push. Alright, so
hopefully you
guys understand this. In the next video, we'll get started on setting up GitHub actions so
that we can actually build out our CICD pipeline. Right before we get started with working with
GitHub
actions, I want you to open up a couple of web pages. So if you go to the GitHub actions
documents
page, I definitely recommend you pull this up. Take a look at the quick start page in the
reference page just to get an idea of how to work with GitHub actions. I will cover everything but
you
know, you are going to forget some things I cover. So definitely know where the documentation
is, pull up the GitHub marketplace as well. So this is where we can look up built in actions that we
can
use in our CICD pipeline that have kind of handled a lot of the more complex logic. And then
on top
of that, you'll see that GitHub has this example tutorial on how to set up a CICD pipeline for
a
Python application. So this is what I kind of looked at to get started with building out a
CICD
pipeline for our fast API. And it kind of covers most of the things we do that we need. So if
you
want to, you could just follow this. And I think at that point, you'll have a good idea as to
how to work with this. But once again, like I said, we're going to cover everything from
scratch.
But if we go to the documentation, you go to quick start right here, it tells you that we want
to create a folder called GitHub. And then in that folder, we want to create a folder
called
workflows. So let's go ahead and do that. So in our main project directory, let me just
minimize
everything, I'm going to create a brand new folder. And I'll call this dot GitHub. And then we
want
a new folder within here called workflows. Alright, and so here, we can define all of our
workflows,
which kind of make up our CICD pipeline, you can have more than one, you don't need to just
have one in this case, but we're just going to use one because we're keeping things simple. And so
here,
this is where we're going to create a yaml file. So if you aren't familiar with yaml, it's
just a certain type of markup language that's commonly used for like, config files and things like
that,
because it's very easy to read. But the most important thing to keep in mind is that spacing
matters in yaml. So you definitely want to make sure that all the things are lined up very
carefully,
just like it was when it comes to like the Docker compose file. So we'll go here, and I'm
going to
create a brand new file. And I'm going to call this build dash deploy dot yaml. Alright, and
so we're
going to give this workflow a name. So even though we gave it the file a name, we actually
have to provide a name here. So we just do name and feel free to call it whatever you want. I'm just
going
to call this build and deploy code. Okay, and then we have to define when should our workflow
or
in this case, which is our CI CD pipeline, when should it run when should it trigger what
triggers our code to run. And so like I mentioned, it's usually a, either a, a pull request or a git
push.
So to specify when we trigger this, you pass in the on field. And then you specify, you
know,
should this be a push or a pull request? Or should it run for both? And so if you want this to
run
only on git pushes, you would just do on push. If you wanted to run on pull request, you do
pull
request here. But if you wanted to run for both, both git push and pull request, then you
would do
push and pull request. And there's a lot of flexibility to so the way we have it set up
right
here, this would happen on every push on every single branch, and every pull request on every
single branch. But there's a possibility that maybe you don't want this to run for all of
your
branches, right? Maybe you only want this to run on the main branch, and a couple of other
branches,
and then the rest of the branches you don't really care about. So what you can do is we can go
down a level, and then hit tab in this case. And remember, like I said, spacing matters. So
we'll
say push. And then here, you can define all of the branches that should run whenever you do a
push.
So if I hear I pass in main, what's going to happen is that this workflow is going to
trigger
every time we do a push, specifically only to the main branch and no other branches. And so
you can
provide a list of all the branches. So this is, you know, another branch. This is, you know,
maybe
some feature branch, all the names of the branches that you want your workflow to run on, you
can specify that. And this is one way of providing a list in YAML. The other way is to actually go
down
here, and then do a dash, and then provide the name. So I would do main, and then, you
know,
some other branch, and so on. And then if we wanted to, we could also customize all of
the
branches that should trigger this workflow on a pull request as well. So if I do pull
request,
and then I do branches, and this should be moved back here. And then here, I do
branches.
And then I can do a list here as well. And I'll just call this, we'll give an example
branch,
I'll say, test branch. Right. And so just to kind of read this through, anytime we do a push
to
either one of these branches, it's going to run our code. And anytime we do a pull request to
this specific branch, it's going to run our code as well. So that's just how to set this up.
We're
not going to do any of that, because we just have one branch in this case. So I'm just going
to say, we're going to have run this on push and pull requests for all branches. And if we
actually
go to the documentation, I think the reference page, you can go to the reference page and
then
select events that trigger workflows. And that's going to cover everything that we just
covered. So it's going to go over all of that. And you'll even see that there's a scheduled event. So
you
can tell it to run kind of like a cron job, like, hey, I want this to run every single hour.
And there's a few other like manual events that you can trigger. And a couple of like
webhook
integrations as well. But we're not going to do any of that, we're just going to keep things
simple for now. Alright, so once we've defined what's going to trigger our workflow, we have to
then
create a job. And so if you go to the documentation, the GitHub actions documentation says
that a job
is just a set of steps that execute on the same runner. So our runner is just a virtual
machine. So when our workflow runs, right, it's going to, we have to provide it a job, which is just a
list
of steps that's going to run on that specific runner in that case. And so here I'll go to
jobs,
this is where we provide a list of jobs, we're going to start out with just one in this case,
and we can give this whatever name we want, I'm going to call this job one, not a very good
name,
we're going to rename it in the future. But I want to just keep it simple for now. And what we
want to do is we want to specify what type of machine do we want to run this on. And
so
like I said, a CI CD tool like GitHub actions is going to provide us a machine, a
computer,
and we have to specify what operating system do we want to run it on. So we're going to just
use it on on Linux. But keep in mind, you can run this on Windows, you can run this on Mac, you can
even
run it on all three if you want to test all of them. But since our deployment, our since
our
production environment is running on a Linux machine, I think it makes sense that we do all of
the testing and all of this on a Linux machine. So you pass in the runs on field. And then
here
we say Ubuntu latest, this is gonna be the latest Ubuntu version. And if we go to the
reference
page here, and go to the workflow syntax, I think, and we go to jobs, that's going to provide
us all
of the information when it comes to what we can run this on. So you can see here, we can
provide Windows 2022. Or you can just grab Windows latest, we've also got, and I realized the text is
probably
small for you guys. We got different versions of Windows, we got Ubuntu latest, which in this
case,
at the moment, it's just going to be the same thing as writing Ubuntu 20.0.4. But you can grab
any one of these specific Ubuntu versions as well as Mac OS 11, and some of the other previous
Mac
versions. But like I said, we're going to stick to just running on Ubuntu, and we might as
well just grab the latest version. And so once we specify the operating system, the next thing
that
we have to do is provide a list of steps. So we kind of went over what the steps would look
like
when we did the little PowerPoint demonstration. So we hear we say steps. And then we provide
a
list of steps. So when it comes to lists in yaml, you just do the dash, and then you provide
the lists. And usually when it comes to a specific step, we give it the specific command that
we
want to run, as well as a name, like a human readable name, so that when we look through
the
logs, we know which step is doing what so we can say like, hey, this step is installing
Python, or this step is running pytest. And so here we say name, this is where we provide a
description
for what we're about to do. So the first thing that we always want to do is we want to check
out our code. And so I'm gonna say, hey, this is checking. Or I'll just say this is pulling
get
repo, right, it's gonna pull our code. And then we have to provide the uses. So this is the
actual
command that needs to run on our machine to pull our repo. And with GitHub actions, like I
said,
there's a marketplace of pre built actions that we can use to, you know, essentially perform
various
tasks. So pulling your code is a very common task. So they've definitely got an action for
this.
And so if I just search for checkout, because I know that's what it's called, you can see this
is the one we want. And so this is, this is what's going to be
responsible for pulling your code. And so here you just say uses, you do actions slash
checkout v2.
And you could specify a specific repository that you want to pull, however, GitHub already
knows
what repository we're working on, because we did a get push. And since all of this is
integrated together, we don't actually have to pass in any other field. However, you know, if you look
at
the documentation, you can specify what specific repository you want, as well as you know, any
SSH
keys that you may need and things like that. But this is GitHub action. So it's all already
hosted on GitHub. So it's a pretty simplified process. So all we have to do is just copy this right
here.
And we can just kind of continue down and just provide as many extra steps as we want. I'm
just going to provide a very simple step after this, just to show you, you know, a step is just
nothing
more than us giving a command. So I'm going to name this one. Say hi to Sanjeev. And what we
can do
is this is a special action that's that we grabbed from the marketplace. But if you just want
to run
a command on the machine, which is just a Linux machine, we can run any specific Linux command
that we want, because we have access to the CLI, all we have to do is just use the run
keyword,
and then we specify the command we want. So if you wanted to install a, you know, a package,
we could do, you know, sudo apt, install whatever, or if you want to reload the machine, you
know,
you could do a reboot, I'm not sure why you would do that. So all I'm just going to do is I'm
going to just make it say echo, which is just going to echo out exactly what I type in here, I'm going
to
say, Hello, Sanjeev. And so it's just going to run this command on our Linux machine. And so
this is
a very simple example so far. But I think that's a good starting point so that we can
actually
understand what's happening. And what we want to do is we now want to actually push this
changes
to our Git repository. So let me open up my terminal. Make sure you do save all. And so
once
we push this, this will actually be part of our repository. And it's going to actually
trigger
GitHub actions to actually trigger this workflow, because we said that on every push, we
should run this workflow. So I'll do git add all. I'll do a git push. Sorry, git commit dash
m
adding first GitHub action. And then we'll do a git push. And we can do an origin main if you
want
to be specific. All right, so it's been pushed. And so now if we go to our GitHub
repository,
there's a actions tab right here. And so you'll see that I had a previous I had a couple of
other
actions that ran because I was doing a dry run. But you'll see that the most recent one, which
is the one we had, which is called adding first GitHub action. So this actually comes
from
the commit name. And we can select this. And we can see exactly what happened. So this gives
us
a little summary, we can see that we have one job called job one. And then we can see that the
job ran and it successfully ran and it took four seconds. And if you click on the job,
you'll see all of the steps. So the first thing is it sets up the job. And so you can see that
it's creating a runner, which is once again, just a virtual machine. And then it's going to
provide
us a button to because we specified that as our operating system. It's going to set up a
virtual
environment, don't worry too much about that. It's going to handle all of the GitHub
permissions because it's all integrated with GitHub. Then we go down to the first action, which was pulling
the
git repo, because we did say action slash checkout v2. So then it's going to pull the GitHub
repo.
And then we could see the second option. Sorry, the second action, which was say hi to
Sanjeev.
So all it's doing is just running this command on our Linux machine. So it's just saying echo,
hello, Sanjeev, and then it just prints out. Hello, Sanjeev. So this is all a CSCD
pipeline
is it's just providing a bunch of instructions for a little machine on the internet to run for
us.
Now, technically, the machine doesn't have to be on the internet, it doesn't have to be
hosted. There's some CICD tools that you host to yourselves on your on your own machine or your own
server,
so that they actually handle running all of this. Because even though GitHub actions is free,
they only give you a certain number of build minutes for free. And then after that,
they'll
start charging for it. So depending on how many times you build, it may actually be more cost
effective to just set it up yourself. And then it looks like, you know, GitHub actions, there's
a
post pulling git repo. So it's going to perform a few other like cleanup actions, and then
it's going to then clean up the job. So we have successfully created our first simple
example,
CICD pipeline or GitHub actions workflow in this case. So let's take our GitHub actions to the
next
step. And I'm going to actually remove this example, I just wanted to show you guys how to do
that.
But after we pull our GitHub repo, the next thing that I want to do is I want to set up
Python. And so to set up Python, once again, we can go back to the marketplace, if I can find
it,
because you know, setting up Python seems like a very common task. So we can just search for
Python.
And keep in mind, when it comes to setting up Python, you could technically just do the same
thing where we can say uses, and then we can, you know, run the command on a Linux machine to
install
Python, which would be something like sudo apt install Python, right, we could 100% do
that.
And that would 100% work. But because this is such a common task, the task, there's already a
pre built action. So we don't even have to do that. And I think I searched Python,
did that work? It didn't look like it worked. And here we go, set up Python down
here.
So this is going to be responsible for setting up and installing Python on our
machine.
And you'll see an example of how to do this, you just say uses, action slash setup Python at
v2. And then you say with and then you specify the version. And then
you can also provide the architecture as well. And what's really cool about this is that,
let's say we wanted to test our code on multiple different versions of Python.
Well, we can go under the strategy section. So right under where it says runs on, we can do
strategy. And then we can provide a matrix and then a list of versions. So if we
want to test version 3.7 version 3.8 3.9 3.6, and make sure that our code works on all of
them,
we can then create this matrix here. And then when we go to set up our
Python,
right, we would say with and then for Python version, instead of providing one specific
version, we actually pass this is a essentially a variable syntax in YAML. And then we just call matrix
dot
Python version, which is referencing this list right here. And so it'll just run our
actions
for all of these versions just to verify that everything works in all of the versions. But
we're going to keep it simple. We're just going to do version 3.9. If you want to play
around with it, I definitely recommend you try testing this out. But for us, it doesn't really
matter. But keep in mind, it looks like you can even do the same thing for different
operating
systems. So if you want to test on Ubuntu, Mac and Windows, you can do the same thing as
well.
But like I said, we're going to keep things very simple. And I'm actually going to change this
to
name, give this a name, I'm going to say install Python version 3.9. And we'll say users and
then
we'll call that action now. So we'll do actions slash set up dash Python at V two. And then
we
pass in with tab. And we'll say Python dash version. And we'll say 3.9. And I think
that's
all we have to do. Let me see the example. Python version 3.9. Yep, that's all we can leave
the
architecture, I'm going to use the default, which looks like it defaults to x64. So not a
problem
in our case. Alright, and then after installing Python, the next thing that I want to
do
is I want to upgrade pip. So I'll create a description of update pip. And then we'll
say
uses actually instead of uses, we're just going to run a command on the Linux shell. So we'll
say
run. And we just run Python dash m pip install dash dash upgrade pip. So this is the command
that
you'd run on any Linux operating system, or even on a Windows machine to upgrade pip. And then
the next thing that I want to do is we're going to install all of our dependencies. So since
we
pulled our Git repo, right with the actions checkout, we're going to have access to all of
this code, and we're going to have access to our requirements at txt. So we have to make sure
that
we install all of these packages there as well. And so here, I'll just say this is install
all
dependencies. And the command that we're going to run for this is going to be a and you guys
should
know this, this is just pip install dash r requirements dot txt. Alright, so let's I
think
this is a good starting point. Let's try committing our code again. I'll just do git add dash
dash
all we'll do a git commit dash m added more steps. And then we'll do a git push origin
main.
Alright, and then we'll go back to actions. And we can see that our workflow is starting to
run.
That's why it's yellow. And we can take a look at its current status. So it's still running
job one, which is the name we gave it. And we can just kind of follow along. So it looks like it pulled
our
repo install whoops, installed Python version 3.9. It's then an updated pip. And that's
already
completed. And now it's installing all the dependencies. So obviously, installing all the
dependencies takes a certain amount of time, that's probably going to be the longest,
specific step in our GitHub workflow at this point. All right, and it looks like that
ran.
And then it's essentially cleaning things up. And then it's completed. And we get the
beautiful green check mark. So we can just close out of this go back. And you can even see the list
of
all of our workflows that GitHub actions is detected. So we have built in deploy code. So
that's the only workflow that we have. But keep in mind, I can create essentially as many
different
yaml files inside this workflow folders to have a different workflow. So I might have one
specific
set of I might have a workflow specifically for one branch and a completely separate workflow
for another branch. It just gives you a lot of customizability. But we're just going to have
one
workflow that works across all of our branches. Now, the next thing that I want to do is I
want to run pytest. So to run pytest, right, it's a pretty simple process. So let me set up the
name
first. I will say, whoops, test with pytest. And then we'll say run. All right, and I need to
run
two commands. So if you ever want to provide a list of commands to run, instead of having to
create multiple different steps, I can do a pipe, go down the line, and just provide the list
of
commands. So we'll do pip install pytest. And then we can go down the line and provide the
second command. So then we just run pytest. And you can, you know, pass in whatever flags if
you
want the dash v and the dash s. But I'm going to keep it simple. And we're just going to keep
it
blank for now. Let's save this. And I'm going to then do the same thing. We'll do a git add
dash
dash all will do a git commit dash m. And I'll say added pytest step. And then we'll do a git
push
origin main again. And so from from a learning perspective, right, this is kind of a slow
process
because we have to push all of our changes. And then now we have to wait for this whole thing
to run. But once you understand how all of this works, and how easy it is to set up, you won't have
to
keep doing this every single time or every single project, you can just essentially copy and
paste most of your workflow. Alright, so it looks like it's completed. And we get a red X. So it
looks
like something broke. And that's to be expected. And it's going to highlight what specific
test failed, or sorry, what specific step failed, but we can see that it was the running pytest. So
if
I just open this up a bit, and then expand it, we can take a look at what the error was. So it
looks
like it installed pytest. And that seems to have worked fine. And then down here, you can see
that there's a couple of validation errors. And so it says that, you know, we didn't provide a
database
host name port. So it looks like there's the issues with environment variables. And that makes
sense, because this is a brand new computer that we haven't touched. So none of the
environment
variables have been set up. So obviously, it has no environment variables. And
our
pedantic validation of environment variables should have failed, and it did absolutely fail.
So this is all to be expected. And so in the next video, we'll figure out how we can set
environment
variables in our workflows so that we can provide the information for reaching our specific
database.
But there's a couple of different ways that we can set up environment variables. And there's a
couple of different places where we can define them. So I'm going to show you a job how to
set
a job specific environment variable. That means this is an environment variable that will only
be accessible from within one job. So in this case, our job that we've been working on is called
job
one. And so if you want to set an environment variable, you just set it right here. So we say
env. And then you provide all of your environment variables. So in this case, let's say we
want
database host name. And in this case, it's going to be local host, we've now successfully
sent
set up one environment variable, and I'm just going to copy and paste all the
others.
And so now we've defined all of our environment variables, in this case,
and that should fix the issue. But I want to show you guys what other environment
variables
that we have as well. And if you want to know where I'm kind of getting all this information,
like I said, we have the documentation, and there should be a section for environment variables
here.
And so I think we should see the same thing, right? Yep, we see the same thing. So ENV under
the specific job. And you just provide the list of all of your environment variables.
And then there's some built in ones. So these are default environment variables that get set
for you. So here you can see, what's the run number? What's the GitHub job? What's the action
path,
we can even see what's a repository. So if you want to get the name of your repository, you
can reference this specific environment variable, but we don't need to do any of that.
But what I do want to show you is, you know, if I had gone in and then created a second job
called
job two, right, this environment variable that we defined here is not available, because this
is
defined under job one. So if you want to have a environment variable that's shared across all
of your jobs, I can specify an environment variable here, I can say ENV. And then I just provide
my
environment variable. So I could just copy this one as an example. And we can provide this. So
then database host name is going to be available for job one, and job two. But for now, we're
just
going to remove that because we don't really need it. We don't really need that right now. And
I'm going to keep everything as default in this case. And we're going to check this into get so
I'm
going to do a git add dash dash all do a git commit dash m added in v variables. And we'll do
a git
push origin main. Okay, and then back to our actions page. If I see the added
environment
variable section, we can see that failed. Once again, this is going to be expected. So if
we
go under, hope it looks like there's a weird failure, no steps defined. Okay, so we got
an
error because I accidentally left job two, so it doesn't like it the fact that I have jobs to
with no steps. So we're going to save this again, and we're going to do another commit. And I'll
just
call this committing again, because I'm getting lazy. Right. And let's just make sure that
we've
kicked this off. Alright, so it looks like you kicked it off. And there's no, like major show
stopping errors at the beginning. But we should still see this fail. And that's to be
expected.
But I just want to make sure that we no longer have issues with environment variables. And I
want
to make sure that the environment variables that we set did successfully get applied to our
runner. Okay, guys, so a couple of things to point out. So there's a whole bunch of errors. And that's to
be
expected. And that's okay. But we can see that it did process all of our environment
variables. So
that's great. So we did fix that issue. But you'll notice that, you know, when we're running
our test, we get a whole bunch of errors. And then eventually, you'll see a whole bunch of SQL alchemy
errors.
And the reason we see a whole bunch of SQL alchemy errors is because if you look at the
environment variables, we're pointing to localhost for our database, there's no database on
this
runner, this is a brand new computer that why would there be a database installed. So we do
eventually have to set up a database for testing on the runner itself, or we would have to
point
it to a remote database, whether that's hosted on Heroku, or AWS, it doesn't matter. But you
know,
for testing purposes in your CI CD pipeline, I'd rather create one on our runner that we can
just quickly spin up for testing. So once again, I just wanted to point out how to set up
environment
variables. The one thing I don't like, and I want to make sure I want to teach you guys is
that, in this case, we're hard coding these values in here. And so this is all stored in our
repository,
anyone can have access to it. This is our test database. So it's not a big deal. But I do want
to show you how we can how we can store these as secrets, so that the runner has access to
them,
but no one else can see them because storing it in here could potentially cause issues. You
know, if you didn't want this, these, these, the the password and the username for your
test
database to get out, in my case, I don't really care. But you know, it is important for you
guys on to understand how to actually do that. So as I mentioned in the last video, our
environment
variables are hard coded into our workflow. So technically, when we check this into Git,
anyone can see it, which may or may not be a problem. But if it is a problem, I'm gonna show you guys
how
we can work with GitHub secrets. So if we go to our GitHub page and go to settings, here,
there's
a section called secrets. So we can define secrets that we can use and access in our
workflows.
And so this is a repository secret. That means it's globally accessible across the
entire
repository across all branches. There's no you know, any kind of environments or anything like
that. So we'll select new repository secret. And here, we just give it a name. So if you take
a
look at our environment variables, I figured, since that's what it's going to represent, we
might as well use the same one. So I'll say database host name, in this case, and then
just
give it a value. So local host, that's it, right? It's just a key value pair, we do add
secret,
and then you'll see it's stored in here. And so now, in our code, if we want to
reference
the this secret, which I happen to call database host name, we just copy the name of the
secret.
And then what we do is we use dollar curly brace curly brace, so two sets of curly braces. And
we
say secrets dot and then the name of the secret. So this will allow the runner to access the
secret
from our GitHub repo, so that our our workflow code doesn't actually expose all of this
information.
And so this is what you want to do for your passwords and things like that, especially when it
comes to storing the information for connecting to your production database, which
we will do in one of the later lessons. But before we proceed any further, I want to actually
show
you another option. So I'm actually going to remove this repository secret in this case, which
is, once again, a global secret that our entire repo can access. Instead, what I like
to
do is I like to set up environments. So you can set up a whole bunch of secrets that are
available depending on what the environment is. So I can create one for testing, one for
development,
one for production, gives us a lot of flexibility. So I'm going to create a new environment.
I'm
going to call this testing. And within the environment, we're right, we're under the
testing
environment, we can add a secret here. And so we're gonna do the same thing, I'm going to call
database host name. And in this case, we'll say, local host, we'll add the secret. And we
can
provide as many secrets as we want. And then what we can do is, in our job, we can specify
what the
environment is for the job. So I can say, and oops, environment. And then we provide the
name,
which is going to be testing, right, this is going to match up with whatever name we gave
here. So I
called it testing. So we give this the name testing, and it's going to have access to all of
the
environment variables that we defined here. And the way to access them is exactly the same way
you just do dollar to curly braces, secrets, and then the name of the environment
variable.
Now, if there's an overlap with the name of your environment variables, when it comes to the
ones that you set in your environment, as well as the one you set globally for the
repository,
I don't actually know who takes precedence. I suspect it's probably the environment, but you
should check the documentation to verify. But we don't have any global secrets, I'm going to
be
using environments for everything. So this keeps things nice and simple. And so now that we've
set
up this one, let's go ahead and just set up all of these while we have the option. And so
we're
going to set the port as well. Here, we'll set the password. And we've got the database
name,
which will just say fast API. Then the database username and autofills, because, you
know,
I ran through this all in a quick trial run before I started recording. So it's kind of cached
in there. All right, and so now that we have all of our environment variables or our
environment
secret set, we could just change all of these to reference those secrets. We just do the
same
process. And in this case, it's just going to be the same exact name as the key in this
case.
Alright, and so now that we got all of these set, let's just save this. And I'm going to check
this
into Git now, just to make sure that you know, everything is kind of working so
far.
And we'll take a look at the job real quick. And hopefully, there's no errors or only errors
that we expect to see. And what did I call this one?
Asdf. Oh, okay. So it looks like it failed already. So we clearly did something
wrong.
This workflow is not valid. unrecognized name database underscore port.
Well,
what happened there? And it even tells me the position. So if I like show
more
database underscore password. So let's see, what did we mess up? Yeah, simple mistake, I
forgot the secrets dot before all of my variables. So that's why
it had no idea what that was, we have to reference secrets to actually get access to
them.
And now let's try this one more time. All right, this looks a little
better.
All right. And so now if we look at the test with pytest, you can see that the environment
variables were passed in and GitHub will automatically kind of hide what all of the
very the secrets are because once again, they're secret, so you shouldn't see them. But it
does show that we were successfully able to retrieve them. And we got the same error down here.
This
is just an issue with accessing the database, because there is no database on on local host.
So it's trying to connect to a database that doesn't exist, which is going to create
all
sorts of issues. So in the next video, we'll figure out what we can do to set up a test
database on our runner so that we can actually run our tests. So when it comes to setting up
a
database on our runner, the easiest way to do that is to create a service container. So what's
going
to happen to GitHub actions is actually going to allow us to spin up a Docker container, which
really is the easiest way to set up Docker on sorry, to set up Postgres on a remote
machine.
That way, we don't have to worry about any issues with potential installation of a Postgres
database. And so I recommend you take a look at this tutorial, which kind of walks you through
how
to set up a Postgres service container. And it just gives you an example in this case, which
we are essentially just going to copy in this case. But here, we just under jobs,
we specify a service, we give this service specific name or label. And then we just
tell
the image that we want to use. And in this case, we're just using the default Postgres image.
This is the one that comes from Docker Hub, by the way, but you could technically use any image that
you
wanted to, but Postgres, so we might as well just use the default one. And then we can pass
environment variables into the Docker container as well. So one of the things that we want to do is set up
the
password for Postgres, as well as even potentially the database name. And then what we want to
do is
we want to set up some health checks to wait until Postgres has started. Because until
Postgres starts, we should not run any tests because the tests are going to fail because the database
is
down. So we want to make sure that Postgres is up and ready before we actually start running
our tests. So we're essentially just going to copy this line here. But let's go back to our code.
And
we'll take a look at how to do this step by step. And so I'll just go under the environment
variable
section here. And we'll create a section called services. And I'm going to create a service
called
Postgres. And we're going to provide the image just like we saw in that example. And that's
going to
be Postgres. And let's pass some environment variables. So the first thing is going to be
the
Postgres password. So we're going to do Postgres underscore password. And here for the
password,
we can literally reference the same exact secret because they're going to be using the same
password.
And then we also want to give it a custom database name. So we want to automatically create
the fast
API underscore test database so that we don't have to do it in code. So I'll do Postgres
underscore
DB. And we'll do dollar dollar. And then here I can pass in with my database name. So this is
just
going to be fast API at this point. But we want fast API underscore test. Because that's what
our
code has been set up to run. So we'll do underscore test, right? So this is kind of like
string
interpolation. So same thing. And then at that point, I think our Docker container is
pretty
much set up, we do have to specify the ports that we want to open up. So we'll say
ports.
And you know, we want to do 5432. And then 5432. So this is what we want. And you're
probably
thinking, well, can we use the the variables here? And I tried to do this, I tried to paste
this here,
and then paste this here. And I actually got errors, I don't know why it didn't work. But for
some reason, this wasn't allowed. So you have to hard code these values,
unfortunately.
And then lastly, let's set up the options. So I'm going to just copy these options right here.
Because we want those health checks for sure. Let's save this and make sure that all the
spacing
lines up because a lot of times the spacings can mess up things. I have a auto format or so
with
for yaml syntax, it'll automatically format it so that all the spacing is set up
properly.
You want to make sure that you do it manually or set up or install a extension in VS code, if
that's not for you. Okay, so we've got our Postgres database,
and I think we should be good to go. So let's try this get add all get commit. All right, and
so we
can see that our most recent test in this case happened to succeed. And if you actually go
to
the test with py test, and just see the results, we could see that 46 tests pass with no
issues.
And so that means it did successfully connect to our database. And after that, it essentially
completed the job. So now we've successfully added a database and we've successfully
set up our CI pipeline, this is the CI portion of things to pull our code, install Python, set
up,
you know, install all the dependencies, and to run our test. So all of this happens in an
automated
fashion now, just by doing a git push. So you could see how easy it is to get things
started.
And so I think from the CI perspective, the last thing I want to show you guys is how
to
essentially create a Docker image in our runner and push that out to to Docker hub so that
our
production network can then pull that brand new image. So there's a couple of things that we
have to do to get Docker working in our GitHub workflow. The first thing is we need to have a
repository
set up on Docker hub to store our images. And if you did not create one during the Docker
section,
go ahead and create a new one. So log into Docker hub, select Create repository and just give
this a name that you can remember. In this case is going to be fast API, I'm going to call it
that
and then make it public just to keep it simple. And so this is our repo. And then it's going
to tell us how to actually push an image to it. And then if you go to this page right here, so
docs
dot Docker calm slash CI CD slash GitHub actions, it's got an excellent tutorial that we can
follow
for setting up Docker with our GitHub actions. And so if we go back to our Docker hub, the
first
thing that we want to do is we want to get an access token. So let's go here. And I think
we
go under settings, or well, it doesn't look like it's here. Maybe account settings. Yep, and
then
security and then we can select new access token. And maybe we can call this GitHub actions.
And
we're going to give it, you know, whatever permission that you want. Does the Docker give us
information on what permissions doesn't look like it. In this case, so just go ahead and
select
all of them for now. Alright, and so it's going to give you your access token. Make sure
that
you don't share this with anyone else. And it's only going to be displayed once. So go ahead
and copy it and make sure that you have this. Then what it's telling us to do is we want to create
a
secret for both our username, as well as the Docker hub access token. So let's create
these
two secrets in GitHub for Docker hub access token and Docker hub username. And I actually
forgot
what my username was. Okay, so it's sloppy networks. And so here we'll go into
settings.
And feel free to use either the global repository secrets or environment secrets. I'm going to
just
put this in the testing environment for now. But you can use either one in this case. And so
we got Docker underscore hub underscore username. This is going to be sloppy networks
for me, but it's gonna be something else for you guys. And the last one is going to be Docker
hub.
What is it Docker hub access token. Right. And so once we get that set up, you'll see that
they're
going to give us the steps on how to actually set this up in our yaml file, you can ignore the
original the the setup process. And what we want to do is we want to go down to the this
section
right here, which shows us how to log into Docker hub. So we can literally just copy this
straight up. And just paste it in here. So we can just say, we'll do it at the what step should we
do
this at? We'll do it at the end, just because why not? Because some of you guys might not
be
interested in setting up Docker. So it's going to be an optional step at the end. So we'll log
into Docker hub using this built in action from the marketplace. And then here, it's just
going
to provide two properties, username and password, which is going to reference the two secrets
that we created. All right, and then we want to set up Docker build x. So we'll copy this one as
well.
All right, and then we have the build and push step right here, which in this case, it's a
once
again, a built in action. And all it's going to do is we're going to provide a path to the
context. So this is essentially where is the Docker file located. And we, this is technically
optional.
But if you want to, you can hard code the specific Docker file. I think it should default to
this one,
because that's the default Docker file. But we'll just put it in just in case. And then the
push is means not only are we going to build the Docker image, we're also going to push it up to
Docker
hub. And then we have to give it a specific tag. And if we go back to Docker hub, and I go to
my
repositories, and I go to my fast API, it's going to show me the format of the tag that I
need,
so that I can actually push it up properly. So it's going to be the username slash, and then
the name of the repository, which is fast API. And then we can also give it an optional tag name as
well.
So once again, we're going to copy this and just update a few fields.
Everything else should be just fine. The only thing that we have to change is this is going to
be fast API. And it's going to reference the username. So it's going to be in the exact
format.
And if you want to give it a very specific tag, you can give that as well. But I'm going to
just give it the default latest. And then here, it's just printing out the image digest, which is
for
us is optional. You'll see that it does provide us some optional things that we can do
for
optimization. So we can use the build cache so that we don't have to read download all of
our
images every time we run because that's going to extend the build time and the runtime for
our
runner, which is eventually going to cause us a certain amount of money. So this is the code
that
you can use. And if you look at the bottom right here, this is going to show us all of the
code at the very end. So we can literally just copy all of this if we wanted to, and just use that
instead
of what we put. So that's what we will do. So let me just delete all of this and then paste it
in.
And we have everything. And once again, we just want to change the name to be the repository
name,
which is right here, we could also use a secret for that. But don't worry too much about
that.
And that should be all that we have to do for the Docker integration. If you don't have a
Docker
file, then I recommend you well, I'll just pause this video, create a file in your root and
the
root of your project directory called Docker file with a capital D and just copy all of this.
Or you can follow what I go through in the Docker section if you guys skip that. And I think
that
should be everything that we need. So let's go ahead and push this and pray that it does
not
break anything. And it works just the way we expect it to. So we'll do get add dash dash all
get commit added Docker. And then we'll do a git push origin. Yep, and it failed. So fully
expected
this. A step cannot have both uses and run keys. So what did we break here? Actually, it
should
where is it telling us this? Yeah, this spacing got messed up. So that was a very obvious
mistake
that I should have caught. I just have to move this back right there. And that should fix
everything.
So let's do this again. Alright, so so far, it's not aired out. So it looks like things look
okay.
And we can see the build and push section failed. So let's see if there's an error. And it
looks like the there's some issue with the driver. Please switch to a different driver
Docker
build x create dash dash use. But let's see what that is in this case. And I realized the
documentation
was a little confusing this section that we copied at the end. It didn't include one of these
steps,
which is the build x step. So before we are actually after we log in, we want to set up a
build x,
which is the mistake that we ran into. So we want to copy this page right here, this section,
and then we'll run that right after the login. And so now if we do a get at all
get commit dash m. And then I get push origin main. Okay, so I think this time, everything
should work.
But just to reiterate, because I think I went a little bit quickly. The steps are we first
log
into Docker using the login action right here. Then the next thing is we used, we set up build
x
we set up build x. And then we call the build and push action in this case. And we here we
specify
the context, which is saying just this current directory, and then the location of our Docker
file. And then we're going to use build x for the builder. And we set push to be true, because
we
want to actually push it to the to the GitHub repo. And then here, the repo is going to be our
username slash, then the repository name, which is happens to be fast API, we could store this in
a
secret, but I decided not to. And then these are just copied from the, the documentation so
that
we can catch all of the images, so that we don't have to keep running all of this from
scratch, which is a fairly long process. And then here, we're just spitting out the image digest. And
if
we take a look at our workflows, and we can see that the last one, which is called asdf
successfully
ran. And if I actually go back to my Docker hub in this case, and just hit refresh,
right,
we can see that we did successfully push an image a minute ago. So that means we have now
successfully
completed our entire CI portion of our CI CI CD pipeline. The next step is to set up
the
continuous delivery, which is a matter of just providing a whole bunch of commands needed
to
actually push out our new code to our production network. And, you know, moving forward, just
to
kind of keep things simple. And as quick as possible, I'm actually going to copy out all of
the Docker steps, because we're not even using Docker in production. So we don't actually
need
this. So we'll just comment that out just to make things as quick and as snappy as possible,
so that we don't use too many of our build minutes. So now that we set up the continuous
integration
part of our CI CD pipeline, it's now time to move on to the continuous delivery
aspect
of our CI CD pipeline. So this part is going to be responsible for pushing out the
update
to our production environment so that our code or our production environment is running our
newest
and latest code. So there's a couple things we can do. And I'm actually going to do this up
here,
actually, we'll just do it down here, it's fine. So what we could do is, you know, just to
keep things simple, I could just provide another step, right? And we can give this, you know,
whatever
name we want, I could say, name, and then hey, this is push changes to production. And then
we
can, you know, provide the steps needed to actually push those changes out. However, instead
of doing
it like this, what I would like to do is I actually want to break it out into a separate job
so that we can kind of have things nice and organized. And what I actually want to do is I want to
rename
job one because it's not very descriptive, I'm going to rename it to build. And then I'm
going
to create a second job. And we'll call this one deploy. So we've got two different jobs.
However,
the, the issue with jobs is that they don't behave the way that you think they would. And let
me
delete this. The way that jobs work is that, by default, in GitHub, the jobs run in
parallel.
And so what I mean by that is that when our workflow runs, GitHub is actually going to
run
the build job, and the deploy job in parallel at the same exact time. Now, we would definitely
not
want to do that, because that would mean before we've even, you know, run our tests and done
anything else, we would try to actually deploy our new changes, which would be a terrible thing
to
happen. So we don't want it to run in parallel. How exactly do we configure that? Well, we
pass in
an option called runs. Sorry, needs. So you say needs. And here we we tell it the list
of
jobs that need to complete before this job can run. So here we just say this needs to wait
for
the build job to run before this job runs. And so we just pass that into
here.
And you could pass in multiple, multiple jobs if you wanted to, we only have one other one. So
this
is going to cause this to run in a sequential order. And just like with the other job, we
need
the runs on property to define, you know, that we want to run this on Ubuntu latest. And then
I'm
just going to create a test step. In this case, we'll do steps. And the first step is going to
say,
uses, actually, I'm just gonna say name, you know, deploying stuff. And here, I'm just gonna
run a
command on our server. And I'll just say echo going to deploy some stuff. All right, so let's
test
this out. I'll do a git add dash dash all git commit dash m. And if we go ahead and check
GitHub,
and go to our actions, let's take a look at what happened in the adding deploy. And so you can
see
now that we have two different jobs, we can actually see the two different jobs here. And so
look like build ran just fine. And then look like deploy also ran just fine. And so if I take a look at
the
deploying stuff, we can see that I ran the command going to deploy some stuff and it was
successfully deployed, although we didn't actually deploy anything. But you get the gist of it. So now
it's
just a matter of figuring out what commands we need to run to actually push those changes out
to our production server. And so you know, we covered two different deployment models in this
course,
there's Heroku, and then there's the Ubuntu server. So we're going to start off by going over
how we can actually handle this with Heroku. And if we go to our code, right here, we're going to
provide
the steps to actually push an update to Heroku. Now, if you don't remember what we actually
did to
push changes out to Heroku, do a git remote, I think, yep, so this is going to list out to
the
the Heroku remote. So if you want to actually push out the changes to your Heroku application,
you would do a git push Heroku, and then your branch, which is main, and that's going to
update
our application. It's pretty simple with Heroku. So in this machine, right in our CI CD
pipeline,
it's not quite as simple as running git push Heroku main just because we don't have
Heroku
installed on this machine, because when we set up Heroku, we had to install the Heroku CLI, we
had to run Heroku login, put in our credentials. And then we had to pull we have to get our
git
repo, we have to add the Heroku remote, and then we have to do a git push. So there's a couple
of different steps. And so manually doing it, I just want to kind of write this out, we would, you
know,
install Heroku CLI. We would, actually, before we even do that, we would, first of all, you
know,
pull our GitHub repo. Because remember, this job is essentially starting everything from
scratch,
potentially on a different machine. So anything we did here, none of this stuff is available.
So we don't actually have access to the repo that we pulled here. So we would have to do it again
here.
So we pulled the repo, we install Heroku CLI, then we do a Heroku login. And then we do
a
we have to add in a remote and get and I forget the command. So but we'll just say add git
remote
for Heroku. And then we have to do a git push Heroku main. So those are the steps that we
would
have to run to actually deploy our application in our CI CD pipeline. And this is all
technically
possible, you can manually do this. But it's actually a kind of a pain in the butt to
actually
do this. And instead, what I recommend you do is look for a a, a action in the GitHub
marketplace
that kind of does this all under the hood. So that's what we're going to do. Because, you
know, deploying to Heroku is a very common task. So if I go to the marketplace, which I forget which
tab
was the marketplace. Oh, I think it's this one. Yeah. And so here, if I search for
Heroku,
you'll see that we have a couple of different options. For deploying to Heroku in the
marketplace,
let me just see view all because I got to figure out which one is the one that I want. And
this is the one that I want by Akilesh and s. So this is the one that's got the most stars. So
we'll
select this one. And you'll see that in the example, the way that we actually deploy to Heroku
is we
have to get a Heroku API key, we have to provide our Heroku app name. And then we have to
provide
our Heroku email. So let's get this from our account. And then we will create secrets
within
our GitHub repository. So we can store that information because we don't want to hardcode that
into our YAML file. So going back to Heroku, if you go to your account, which is the
little
logo here, then select account settings, you'll see that the API key is right here. So we can
just
say reveal. And then I'm just going to copy this key. And what we're going to do is we're
going to go to our GitHub repository, we'll go to settings. And then we can create our secret. So you
can
create our secret. So you can create a global repository secret. Or you can do what I'm
going
to do, which is I'm going to create a brand new environment for production. We'll call this
production. That is not how you spell production. And we're going to add a secret. And I'm going
to
well paste the value here. But the name I'm going to just stick to what they used, just to
make it easier to remember. So Heroku underscore API key add secret. And then what else do we
need,
we have to add the email. So let's create a secret for the email as well. Because why
not,
they didn't do that. But we're going to be a little bit more secure. And I'll call this
Heroku
underscore email. And this is just going to be your email that you use to register. And then
if
you want to, you can also add the Heroku app name as a secret as well. You don't have to do
this,
but I'm going to do it. And what did I call my app, I can't even remember. Fast
API,
Sanjeev yours is going to be different. So make sure you update it accordingly. Okay,
so
wait, what happened here? Why is there only two? I think I may have accidentally overridden
one of
these keys. So let me let me put this information back in just in case because you can't
actually
look at the keys after you save them, you can only update them. Okay, so we updated that and
then
the API key, I have to copy this once more.
All right, so we've got those. And then let me once again, add the Heroku app name, this is
going
to be Alright, so now we've got our three keys, hopefully I set those all up correctly. And
keep
in mind the name of this environment is called production. So for the other job, which is
the
deploy job, we're going to specify the environment, just same way we did with the first job,
just by
referencing this code right here and just updating the name. And so this is going to be
production.
And then we can remove this unnecessary step. And actually, I'll change this to
be
deploying to Heroku. And then we'll change this to be used. And so just like in the
documentation, actually, I might as well just copy it because it's a little
bit easier. So I can just copy this. And we can just update the app name with the secret that
we
defined. As well as the secret for the email.
And that's all we have to do for the Heroku side of things we don't it's as simple as that.
So
that's why I always recommend you look for a built in action, so that you don't have to
perform all of these steps by yourself. And I realized before we even do that, we have to actually
pull
our code, just like we did in the first job. So we're going to copy that section right here
where
we pull the repo. All right, so we've got the repo, and then we deploy to Heroku. And so what
I'm
going to do now is we want to check our code real quick, and make some changes that that'll be
easy
to kind of quickly just test and see that we actually pushed out of changes. So under our
main.py file, you know, I'm just gonna do what I always do. I'm just gonna add
successfully
deployed from CI CD pipeline. Okay, and so I'm gonna save that. And then now we want
to
do a git add dash dash all get commit dash m push to Heroku example. And then we'll do a git
push
origin main. And before I do that, let me go to my app. So if I actually go to my app on
Heroku,
we can see it just says message Hello, world. And so after we commit these changes, we should
see it send the new message message. So I'm going to send going to push this to GitHub now. And
our
CI CD pipeline should run and push those changes out. Alright, so now if I go back to GitHub,
and
we take a look at the actions, I can see my push to Heroku example succeeded. And so you can
see
both the build phase and the deploy phase both succeeded. And if we want to take a look at the
notes, and then just see what happened with the deploying to Heroku section, we'll see the
logs
for this specific step. And so it looks like we can see Heroku is running in this case, and
it
looks like everything worked. So if I go back to my app, and hit refresh, right, we could see
we
get the new message. So we have successfully updated Heroku using our CI CD pipeline. It's
as
simple as that. And before I wrap up this video, this section or this video on deploying to
Heroku,
I found this one good article that shows you how to deploy to Heroku in a manual way. So if
you
don't want to use the built in, or the pre configured GitHub action, you can actually
create
a custom workflow when it comes to, you know, actually manually logging in, and then
pushing
all of these changes out to Heroku, the manual way instead of using that pre built action.
But
this is a little bit more complex. And this would only really be needed under very specific
circumstances, or just for learning, or just for learning purposes. So take a look at if you
want
to see how it's done manually, but you never actually have to do that. Now, before we move any
further, there's one thing that I want to check to make sure that our CI CD pipeline
properly
does. And that is that if we get any kind of errors in the build job, I want to make sure that
the deploy job does not run, right. And so when I say errors, it could be anything, right,
maybe
we're unable to install all of our dependencies, or maybe, and more importantly, if our tests
fail,
right, if my tests fail, I don't want to push my broken code out to production. So I want to
make
sure that if the test fails, we stop right there. And we don't proceed any further with the
deploy.
And so from a configuration perspective, you don't technically have to do anything. That's the
default configuration. But I do want to verify that it actually does what we expect it to. So
in
the tests files, or if I can find it, tests, I'm going to go to, you know, it doesn't really
matter
what test, I'm just going to go on to test posts. And I'm just going to modify this to an
incorrect
value. I'm gonna put this in the notes, change to this one. So I remember which one I
changed,
so I can change it back from 200. So this should cause this test to fail. And if the test
fails,
then we want to make sure that we don't deploy. And I'm also going to go back to our main.py
file
to actually make some visible changes by changing this back to Hello World, to see if our
code
actually got updated accidentally or not. So we got everything set. We'll do a git add once
again.
We'll do a git commit dash m broke stuff. And then we'll do a git or git push origin
main.
And we'll let that run. Alright, so going back to GitHub. Now, let me go back to these, the
actions
page. And we can see the last run, it looks like it failed. So let's see where it failed. And
so
you could see here, it failed in the build stage, and then the deploy stage, I select this,
this
check was skipped. So that means we actually did properly skip the deploy phase because the
build
phase failed. And we can see that we exited with an error code because one of our tests
failed.
Awesome. So let's go back. Let's fix this code real quick. And it was a test posts. And
we'll
change that back to a 200. I don't need those notes anymore. And now let's push out those
changes
once more. Alright, and we'll go back to GitHub again. And let's go back to actions. And
let's
take a look and we can see the last one did get deployed built and deployed successfully. So
if
I go to Heroku, refresh this, we should see that this gets updated just fine. Right. So now
that
we can deploy our updates to Heroku, let's take a look at how we can deploy our updates to
our
Ubuntu server. So once again, it's just a matter of figuring out what exactly are the
commands
that we would need to run to update our production environment. So there's going to be a
couple
things. The first thing is, first of all, we would have to log into our Ubuntu server. And
then
once we log in, when I say a login to our bunches, or I mean, like SSH to it, we would then
have to
move into our slash app slash source directory. And then we would run a git pool so that we
can
pull the latest code and then we'd have to do a a system CTL restart API. So how exactly do
we
do all of this? How do we log into our bunches server? How do we SSH to it? Well, there's a
there's a really nice built in action. I can find the marketplace again. And I believe it's
called
SSH dash action. And I don't think that's I can't find it in here the one I used. But it's by
Apple
Boy. And I'll just do Apple Boy SSH. So search for that. That's there we go. This is the one
we want
SSH remote command. So this will allow you to run commands on a remote machine. So let's take
a look
at how to do this. Here, they give you an example. So you have to pass in the host, which is
the IP
address, the username and the password. And then the SSH port is optional. But I think it
should
default to to port 22, which is the one that's running on our bunches server. So we don't
actually
have to define that. But that's all we really need. And then the script is going to be the
commands that we want to run on the remote machine. So let's set this up. And I'll go down to here. And
I'll
say name and deploy to Ubuntu server. And we're gonna say uses Apple Boy slash SSH dash action
at
master. And then with the first we have to provide a host. And I'm going to store this in a
secret.
And we'll call this what do you think we should call this about a bun to underscore host.
Actually,
we'll call this prod host. And then username, same thing, we're going to store this in a
secret. So
we'll do secrets dot prod, username, and then password. Once again, in another secret. And
I'll
call this prod password. Alright, then we have to provide the commands that we want to run.
And so
since we're going to use more than one command, we're going to do the little pipe symbol. And
then
here, we just list out the command. So the first thing that we want to do is we need to move
into our app directory. So it's going to be the app slash source. And make sure there is no front
slash
that's going to break things. Then we want to do a Git pull. Now this is where it might get
a
little interesting. I'm going to do it the wrong way first. So we do a system CTL restart
API,
but we do need pseudo privilege. So we'll do pseudo restart API, or whatever you call it
yours. And
so let's define these secrets real quick. And we'll go into the environments and then we'll go
to our production environment. And so just go ahead and just create them here. All right, and
then
we're gonna put in our password, I'm not going to show this one to you guys. So I'm just going
to pause the video and then add it myself. And then finally, we need the the username. And I
believe
I called mine just Sanjeev. I think that should work hopefully. And so now we've got all of
our
secrets. And I think we should be good to go prod host username password. Let's try this.
Actually,
before we do that, I'm going to make one more change to the codes that we can actually see the
changes. And so this is pushing out to Ubuntu. And we're gonna have to do a git add once
again,
and then git commit and then git push origin main. All right, and let's go back now. Let's
check on
our actions. And it looks like it's still running. So it's in the deploy phase. And it looks
like
it's connecting up and it looks like it just finished. And it failed. So what exactly
happened
here? If we take a look at the logs, it says, pseudo, a terminal is required to read the
password.
So it's basically saying that when you run the pseudo command, you know, you get the prompt so
that you can input the pseudo password. And since this is all through a remote SSH
connection,
that's not really an option. So we have to pass in the dash s option, the dash s flag. And so
the
way we actually define this is going to be actually pretty interesting. But let's see, where
is my
file. So to use the dash s flag, what we're going to do is we're going to say echo. And then
we're
going to echo the password, our pseudo password, and then we pipe into the next command and we
do
pseudo dash s. So the dash s means that we're going to accept what the password as an
argument,
essentially. And the this pipe command, what it's doing is we, we run echo and then whatever
our
password is. So this we actually replace with the actual password. And it's going to pass that
into
the input of this command. So it's going to pass it into the dash s flag, essentially. So
that's
how you do this with a one line command. Here, we're just going to pass our same
environment
variable. So we'll do secrets dot prod password. And I think that should fix it actually. So
we'll
do a git add dash dash all git commit dash m password fix and git push. Okay, so let's go
back
to our actions. We can see that the password fix commit ran just fine and is succeeded to the
build
phase and the deploy phase succeeded. So if I go to my Ubuntu server, and I and we take a look
at
this, we can see that we did get the updated code. So we did successfully update our
production
server. And on top of that, technically, since we never took out the code for Heroku, we
should see
Heroku also deploy. So if I go to my Heroku app, we can see that pushed out to Ubuntu. So we
now
have successfully completed our CI CD pipeline, we set up all of the continuous integration
steps,
which included copying our source code, installing all of the Python dependencies, testing
our
application, and then we set up the CD phase, which was just a matter of pushing those changes
out to our production environment.